From bc296d05fb83c901d8f36ac11c971ab94d4ed1db Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 16:57:18 -1000 Subject: [PATCH 001/394] wip --- esphome/components/api/api_connection.cpp | 12 +- .../components/climate/climate_mode_bitmask.h | 101 ++++++++++++ esphome/components/climate/climate_traits.h | 128 +++++++++------ esphome/core/enum_bitmask.h | 155 ++++++++++++++++++ 4 files changed, 336 insertions(+), 60 deletions(-) create mode 100644 esphome/components/climate/climate_mode_bitmask.h create mode 100644 esphome/core/enum_bitmask.h diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 7c135946f8..6f6bd27e6e 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -669,18 +669,18 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION); // Current feature flags and other supported parameters msg.feature_flags = traits.get_feature_flags(); - msg.supported_modes = &traits.get_supported_modes_for_api_(); + msg.supported_modes = &traits.get_supported_modes(); msg.visual_min_temperature = traits.get_visual_min_temperature(); msg.visual_max_temperature = traits.get_visual_max_temperature(); msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); msg.visual_min_humidity = traits.get_visual_min_humidity(); msg.visual_max_humidity = traits.get_visual_max_humidity(); - msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_(); - msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_(); - msg.supported_presets = &traits.get_supported_presets_for_api_(); - msg.supported_custom_presets = &traits.get_supported_custom_presets_for_api_(); - msg.supported_swing_modes = &traits.get_supported_swing_modes_for_api_(); + msg.supported_fan_modes = &traits.get_supported_fan_modes(); + msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes(); + msg.supported_presets = &traits.get_supported_presets(); + msg.supported_custom_presets = &traits.get_supported_custom_presets(); + msg.supported_swing_modes = &traits.get_supported_swing_modes(); return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } diff --git a/esphome/components/climate/climate_mode_bitmask.h b/esphome/components/climate/climate_mode_bitmask.h new file mode 100644 index 0000000000..236d153659 --- /dev/null +++ b/esphome/components/climate/climate_mode_bitmask.h @@ -0,0 +1,101 @@ +#pragma once + +#include "esphome/core/enum_bitmask.h" +#include "climate_mode.h" + +namespace esphome { +namespace climate { + +// Type aliases for climate enum bitmasks +// These replace std::set to eliminate red-black tree overhead + +using ClimateModeMask = EnumBitmask; // 7 values (OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO) +using ClimateFanModeMask = + EnumBitmask; // 10 values (ON, OFF, AUTO, LOW, MEDIUM, HIGH, MIDDLE, FOCUS, DIFFUSE, QUIET) +using ClimateSwingModeMask = EnumBitmask; // 4 values (OFF, BOTH, VERTICAL, HORIZONTAL) +using ClimatePresetMask = + EnumBitmask; // 8 values (NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY) + +} // namespace climate +} // namespace esphome + +// Template specializations for enum-to-bit conversions +// All climate enums are sequential starting from 0, so conversions are trivial + +namespace esphome { + +// ClimateMode specialization (7 values: 0-6) +template<> constexpr int EnumBitmask::enum_to_bit(climate::ClimateMode mode) { + return static_cast(mode); // Direct mapping: enum value = bit position +} + +template<> constexpr climate::ClimateMode EnumBitmask::bit_to_enum(int bit) { + // Compile-time lookup array mapping bit positions to enum values + static constexpr climate::ClimateMode MODES[] = { + climate::CLIMATE_MODE_OFF, // bit 0 + climate::CLIMATE_MODE_HEAT_COOL, // bit 1 + climate::CLIMATE_MODE_COOL, // bit 2 + climate::CLIMATE_MODE_HEAT, // bit 3 + climate::CLIMATE_MODE_FAN_ONLY, // bit 4 + climate::CLIMATE_MODE_DRY, // bit 5 + climate::CLIMATE_MODE_AUTO, // bit 6 + }; + return (bit >= 0 && bit < 7) ? MODES[bit] : climate::CLIMATE_MODE_OFF; +} + +// ClimateFanMode specialization (10 values: 0-9) +template<> constexpr int EnumBitmask::enum_to_bit(climate::ClimateFanMode mode) { + return static_cast(mode); // Direct mapping: enum value = bit position +} + +template<> constexpr climate::ClimateFanMode EnumBitmask::bit_to_enum(int bit) { + static constexpr climate::ClimateFanMode MODES[] = { + climate::CLIMATE_FAN_ON, // bit 0 + climate::CLIMATE_FAN_OFF, // bit 1 + climate::CLIMATE_FAN_AUTO, // bit 2 + climate::CLIMATE_FAN_LOW, // bit 3 + climate::CLIMATE_FAN_MEDIUM, // bit 4 + climate::CLIMATE_FAN_HIGH, // bit 5 + climate::CLIMATE_FAN_MIDDLE, // bit 6 + climate::CLIMATE_FAN_FOCUS, // bit 7 + climate::CLIMATE_FAN_DIFFUSE, // bit 8 + climate::CLIMATE_FAN_QUIET, // bit 9 + }; + return (bit >= 0 && bit < 10) ? MODES[bit] : climate::CLIMATE_FAN_ON; +} + +// ClimateSwingMode specialization (4 values: 0-3) +template<> constexpr int EnumBitmask::enum_to_bit(climate::ClimateSwingMode mode) { + return static_cast(mode); // Direct mapping: enum value = bit position +} + +template<> constexpr climate::ClimateSwingMode EnumBitmask::bit_to_enum(int bit) { + static constexpr climate::ClimateSwingMode MODES[] = { + climate::CLIMATE_SWING_OFF, // bit 0 + climate::CLIMATE_SWING_BOTH, // bit 1 + climate::CLIMATE_SWING_VERTICAL, // bit 2 + climate::CLIMATE_SWING_HORIZONTAL, // bit 3 + }; + return (bit >= 0 && bit < 4) ? MODES[bit] : climate::CLIMATE_SWING_OFF; +} + +// ClimatePreset specialization (8 values: 0-7) +template<> constexpr int EnumBitmask::enum_to_bit(climate::ClimatePreset preset) { + return static_cast(preset); // Direct mapping: enum value = bit position +} + +template<> constexpr climate::ClimatePreset EnumBitmask::bit_to_enum(int bit) { + static constexpr climate::ClimatePreset PRESETS[] = { + climate::CLIMATE_PRESET_NONE, // bit 0 + climate::CLIMATE_PRESET_HOME, // bit 1 + climate::CLIMATE_PRESET_AWAY, // bit 2 + climate::CLIMATE_PRESET_BOOST, // bit 3 + climate::CLIMATE_PRESET_COMFORT, // bit 4 + climate::CLIMATE_PRESET_ECO, // bit 5 + climate::CLIMATE_PRESET_SLEEP, // bit 6 + climate::CLIMATE_PRESET_ACTIVITY, // bit 7 + }; + return (bit >= 0 && bit < 8) ? PRESETS[bit] : climate::CLIMATE_PRESET_NONE; +} + +} // namespace esphome diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 2962a147d7..45287689c9 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -1,9 +1,26 @@ #pragma once -#include +#include #include "climate_mode.h" +#include "climate_mode_bitmask.h" #include "esphome/core/helpers.h" +namespace esphome { +namespace climate { + +// Lightweight linear search for small vectors (1-20 items) +// Avoids std::find template overhead +template inline bool vector_contains(const std::vector &vec, const T &value) { + for (const auto &item : vec) { + if (item == value) + return true; + } + return false; +} + +} // namespace climate +} // namespace esphome + namespace esphome { #ifdef USE_API @@ -107,48 +124,68 @@ class ClimateTraits { } } - void set_supported_modes(std::set modes) { this->supported_modes_ = std::move(modes); } - void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); } - bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); } - const std::set &get_supported_modes() const { return this->supported_modes_; } + void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } + void set_supported_modes(std::initializer_list modes) { + this->supported_modes_ = ClimateModeMask(modes); + } + void add_supported_mode(ClimateMode mode) { this->supported_modes_.add(mode); } + bool supports_mode(ClimateMode mode) const { return this->supported_modes_.contains(mode); } + const ClimateModeMask &get_supported_modes() const { return this->supported_modes_; } - void set_supported_fan_modes(std::set modes) { this->supported_fan_modes_ = std::move(modes); } - void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); } - void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.insert(mode); } - bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); } + void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; } + void set_supported_fan_modes(std::initializer_list modes) { + this->supported_fan_modes_ = ClimateFanModeMask(modes); + } + void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.add(mode); } + void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.push_back(mode); } + bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.contains(fan_mode); } bool get_supports_fan_modes() const { return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty(); } - const std::set &get_supported_fan_modes() const { return this->supported_fan_modes_; } + const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; } - void set_supported_custom_fan_modes(std::set supported_custom_fan_modes) { + void set_supported_custom_fan_modes(std::vector supported_custom_fan_modes) { this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes); } - const std::set &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } + void set_supported_custom_fan_modes(std::initializer_list modes) { + this->supported_custom_fan_modes_ = modes; + } + const std::vector &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { - return this->supported_custom_fan_modes_.count(custom_fan_mode); + return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode); } - void set_supported_presets(std::set presets) { this->supported_presets_ = std::move(presets); } - void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); } - void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.insert(preset); } - bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); } + void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } + void set_supported_presets(std::initializer_list presets) { + this->supported_presets_ = ClimatePresetMask(presets); + } + void add_supported_preset(ClimatePreset preset) { this->supported_presets_.add(preset); } + void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.push_back(preset); } + bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.contains(preset); } bool get_supports_presets() const { return !this->supported_presets_.empty(); } - const std::set &get_supported_presets() const { return this->supported_presets_; } + const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; } - void set_supported_custom_presets(std::set supported_custom_presets) { + void set_supported_custom_presets(std::vector supported_custom_presets) { this->supported_custom_presets_ = std::move(supported_custom_presets); } - const std::set &get_supported_custom_presets() const { return this->supported_custom_presets_; } + void set_supported_custom_presets(std::initializer_list presets) { + this->supported_custom_presets_ = presets; + } + const std::vector &get_supported_custom_presets() const { return this->supported_custom_presets_; } bool supports_custom_preset(const std::string &custom_preset) const { - return this->supported_custom_presets_.count(custom_preset); + return vector_contains(this->supported_custom_presets_, custom_preset); } - void set_supported_swing_modes(std::set modes) { this->supported_swing_modes_ = std::move(modes); } - void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); } - bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); } + void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } + void set_supported_swing_modes(std::initializer_list modes) { + this->supported_swing_modes_ = ClimateSwingModeMask(modes); + } + void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.add(mode); } + bool supports_swing_mode(ClimateSwingMode swing_mode) const { + return this->supported_swing_modes_.contains(swing_mode); + } bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); } - const std::set &get_supported_swing_modes() const { return this->supported_swing_modes_; } + const ClimateSwingModeMask &get_supported_swing_modes() const { return this->supported_swing_modes_; } float get_visual_min_temperature() const { return this->visual_min_temperature_; } void set_visual_min_temperature(float visual_min_temperature) { @@ -179,42 +216,25 @@ class ClimateTraits { void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; } protected: -#ifdef USE_API - // The API connection is a friend class to access internal methods - friend class api::APIConnection; - // These methods return references to internal data structures. - // They are used by the API to avoid copying data when encoding messages. - // Warning: Do not use these methods outside of the API connection code. - // They return references to internal data that can be invalidated. - const std::set &get_supported_modes_for_api_() const { return this->supported_modes_; } - const std::set &get_supported_fan_modes_for_api_() const { return this->supported_fan_modes_; } - const std::set &get_supported_custom_fan_modes_for_api_() const { - return this->supported_custom_fan_modes_; - } - const std::set &get_supported_presets_for_api_() const { return this->supported_presets_; } - const std::set &get_supported_custom_presets_for_api_() const { return this->supported_custom_presets_; } - const std::set &get_supported_swing_modes_for_api_() const { return this->supported_swing_modes_; } -#endif - void set_mode_support_(climate::ClimateMode mode, bool supported) { if (supported) { - this->supported_modes_.insert(mode); + this->supported_modes_.add(mode); } else { - this->supported_modes_.erase(mode); + this->supported_modes_.remove(mode); } } void set_fan_mode_support_(climate::ClimateFanMode mode, bool supported) { if (supported) { - this->supported_fan_modes_.insert(mode); + this->supported_fan_modes_.add(mode); } else { - this->supported_fan_modes_.erase(mode); + this->supported_fan_modes_.remove(mode); } } void set_swing_mode_support_(climate::ClimateSwingMode mode, bool supported) { if (supported) { - this->supported_swing_modes_.insert(mode); + this->supported_swing_modes_.add(mode); } else { - this->supported_swing_modes_.erase(mode); + this->supported_swing_modes_.remove(mode); } } @@ -226,12 +246,12 @@ class ClimateTraits { float visual_min_humidity_{30}; float visual_max_humidity_{99}; - std::set supported_modes_ = {climate::CLIMATE_MODE_OFF}; - std::set supported_fan_modes_; - std::set supported_swing_modes_; - std::set supported_presets_; - std::set supported_custom_fan_modes_; - std::set supported_custom_presets_; + climate::ClimateModeMask supported_modes_{climate::CLIMATE_MODE_OFF}; + climate::ClimateFanModeMask supported_fan_modes_; + climate::ClimateSwingModeMask supported_swing_modes_; + climate::ClimatePresetMask supported_presets_; + std::vector supported_custom_fan_modes_; + std::vector supported_custom_presets_; }; } // namespace climate diff --git a/esphome/core/enum_bitmask.h b/esphome/core/enum_bitmask.h new file mode 100644 index 0000000000..9c208f9efb --- /dev/null +++ b/esphome/core/enum_bitmask.h @@ -0,0 +1,155 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace esphome { + +/// Generic bitmask for storing a set of enum values efficiently. +/// Replaces std::set to eliminate red-black tree overhead (~586 bytes per instantiation). +/// +/// Template parameters: +/// EnumType: The enum type to store (must be uint8_t-based) +/// MaxBits: Maximum number of bits needed (auto-selects uint8_t/uint16_t/uint32_t) +/// +/// Requirements: +/// - EnumType must be an enum with sequential values starting from 0 +/// - Specialization must provide enum_to_bit() and bit_to_enum() static methods +/// - MaxBits must be sufficient to hold all enum values +/// +/// Example usage: +/// using ClimateModeMask = EnumBitmask; +/// ClimateModeMask modes({CLIMATE_MODE_HEAT, CLIMATE_MODE_COOL}); +/// if (modes.contains(CLIMATE_MODE_HEAT)) { ... } +/// for (auto mode : modes) { ... } // Iterate over set bits +/// +/// Design notes: +/// - Uses compile-time type selection for optimal size (uint8_t/uint16_t/uint32_t) +/// - Iterator converts bit positions to actual enum values during traversal +/// - All operations are constexpr-compatible for compile-time initialization +/// - Drop-in replacement for std::set with simpler API +/// +template class EnumBitmask { + public: + // Automatic bitmask type selection based on MaxBits + // ≤8 bits: uint8_t, ≤16 bits: uint16_t, otherwise: uint32_t + using bitmask_t = + typename std::conditional<(MaxBits <= 8), uint8_t, + typename std::conditional<(MaxBits <= 16), uint16_t, uint32_t>::type>::type; + + constexpr EnumBitmask() = default; + + /// Construct from initializer list: {VALUE1, VALUE2, ...} + constexpr EnumBitmask(std::initializer_list values) { + for (auto value : values) { + this->add(value); + } + } + + /// Add a single enum value to the set + constexpr void add(EnumType value) { this->mask_ |= (static_cast(1) << enum_to_bit(value)); } + + /// Add multiple enum values from initializer list + constexpr void add(std::initializer_list values) { + for (auto value : values) { + this->add(value); + } + } + + /// Remove an enum value from the set + constexpr void remove(EnumType value) { this->mask_ &= ~(static_cast(1) << enum_to_bit(value)); } + + /// Clear all values from the set + constexpr void clear() { this->mask_ = 0; } + + /// Check if the set contains a specific enum value + constexpr bool contains(EnumType value) const { + return (this->mask_ & (static_cast(1) << enum_to_bit(value))) != 0; + } + + /// Count the number of enum values in the set + constexpr size_t size() const { + // Brian Kernighan's algorithm - efficient for sparse bitmasks + // Typical case: 2-4 modes out of 10 possible + bitmask_t n = this->mask_; + size_t count = 0; + while (n) { + n &= n - 1; // Clear the least significant set bit + count++; + } + return count; + } + + /// Check if the set is empty + constexpr bool empty() const { return this->mask_ == 0; } + + /// Iterator support for range-based for loops and API encoding + /// Iterates over set bits and converts bit positions to enum values + class Iterator { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = EnumType; + using difference_type = std::ptrdiff_t; + using pointer = const EnumType *; + using reference = EnumType; + + constexpr Iterator(bitmask_t mask, int bit) : mask_(mask), bit_(bit) { advance_to_next_set_bit_(); } + + constexpr EnumType operator*() const { return bit_to_enum(bit_); } + + constexpr Iterator &operator++() { + ++bit_; + advance_to_next_set_bit_(); + return *this; + } + + constexpr bool operator==(const Iterator &other) const { return bit_ == other.bit_; } + + constexpr bool operator!=(const Iterator &other) const { return !(*this == other); } + + private: + constexpr void advance_to_next_set_bit_() { bit_ = find_next_set_bit(mask_, bit_); } + + bitmask_t mask_; + int bit_; + }; + + constexpr Iterator begin() const { return Iterator(mask_, 0); } + constexpr Iterator end() const { return Iterator(mask_, MaxBits); } + + /// Get the raw bitmask value for optimized operations + constexpr bitmask_t get_mask() const { return this->mask_; } + + /// Check if a specific enum value is present in a raw bitmask + /// Useful for checking intersection results without creating temporary objects + static constexpr bool mask_contains(bitmask_t mask, EnumType value) { + return (mask & (static_cast(1) << enum_to_bit(value))) != 0; + } + + /// Get the first enum value from a raw bitmask + /// Used for optimizing intersection logic (e.g., "pick first suitable mode") + static constexpr EnumType first_value_from_mask(bitmask_t mask) { return bit_to_enum(find_next_set_bit(mask, 0)); } + + /// Find the next set bit in a bitmask starting from a given position + /// Returns the bit position, or MaxBits if no more bits are set + static constexpr int find_next_set_bit(bitmask_t mask, int start_bit) { + int bit = start_bit; + while (bit < MaxBits && !(mask & (static_cast(1) << bit))) { + ++bit; + } + return bit; + } + + protected: + // Must be provided by template specialization + // These convert between enum values and bit positions (0, 1, 2, ...) + static constexpr int enum_to_bit(EnumType value); + static constexpr EnumType bit_to_enum(int bit); + + bitmask_t mask_{0}; +}; + +} // namespace esphome From a59fdd8e04b7dd9dbf2f97d1eccb0ab15acd7889 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 16:58:15 -1000 Subject: [PATCH 002/394] wip --- esphome/components/climate/climate.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 87d03f78c5..0e49c443c6 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -385,7 +385,7 @@ void Climate::save_state_() { if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) { state.uses_custom_fan_mode = true; const auto &supported = traits.get_supported_custom_fan_modes(); - // std::set has consistent order (lexicographic for strings) + // std::vector maintains insertion order size_t i = 0; for (const auto &mode : supported) { if (mode == custom_fan_mode) { @@ -402,7 +402,7 @@ void Climate::save_state_() { if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) { state.uses_custom_preset = true; const auto &supported = traits.get_supported_custom_presets(); - // std::set has consistent order (lexicographic for strings) + // std::vector maintains insertion order size_t i = 0; for (const auto &preset : supported) { if (preset == custom_preset) { @@ -553,7 +553,7 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { climate->fan_mode = this->fan_mode; } if (!traits.get_supported_custom_fan_modes().empty() && this->uses_custom_fan_mode) { - // std::set has consistent order (lexicographic for strings) + // std::vector maintains insertion order const auto &modes = traits.get_supported_custom_fan_modes(); if (custom_fan_mode < modes.size()) { size_t i = 0; @@ -570,7 +570,7 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { climate->preset = this->preset; } if (!traits.get_supported_custom_presets().empty() && uses_custom_preset) { - // std::set has consistent order (lexicographic for strings) + // std::vector maintains insertion order const auto &presets = traits.get_supported_custom_presets(); if (custom_preset < presets.size()) { size_t i = 0; From dfa51a5137d2463c7f1dfd022b6951032e1b126f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 17:16:04 -1000 Subject: [PATCH 003/394] merge --- esphome/components/api/api.proto | 12 ++-- esphome/components/api/api_pb2.h | 12 ++-- esphome/components/bedjet/bedjet_const.h | 3 +- .../bedjet/climate/bedjet_climate.h | 2 +- .../components/climate/climate_mode_bitmask.h | 63 +++++++++++++------ esphome/components/climate/climate_traits.h | 6 ++ esphome/components/climate_ir/climate_ir.h | 19 +++--- esphome/components/haier/haier_base.cpp | 18 +++++- esphome/components/haier/haier_base.h | 11 ++-- esphome/components/haier/hon_climate.cpp | 6 +- esphome/components/heatpumpir/heatpumpir.h | 11 ++-- esphome/components/midea/air_conditioner.h | 34 +++++++--- esphome/components/toshiba/toshiba.h | 6 +- .../components/tuya/climate/tuya_climate.cpp | 14 ++--- esphome/core/enum_bitmask.h | 2 +- 15 files changed, 137 insertions(+), 82 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index d202486cfa..fae0f2e75a 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -989,7 +989,7 @@ message ListEntitiesClimateResponse { bool supports_current_temperature = 5; // Deprecated: use feature_flags bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags - repeated ClimateMode supported_modes = 7 [(container_pointer) = "std::set"]; + repeated ClimateMode supported_modes = 7 [(container_pointer_no_template) = "climate::ClimateModeMask"]; float visual_min_temperature = 8; float visual_max_temperature = 9; float visual_target_temperature_step = 10; @@ -998,11 +998,11 @@ message ListEntitiesClimateResponse { // Deprecated in API version 1.5 bool legacy_supports_away = 11 [deprecated=true]; bool supports_action = 12; // Deprecated: use feature_flags - repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer) = "std::set"]; - repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer) = "std::set"]; - repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::set"]; - repeated ClimatePreset supported_presets = 16 [(container_pointer) = "std::set"]; - repeated string supported_custom_presets = 17 [(container_pointer) = "std::set"]; + repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer_no_template) = "climate::ClimateFanModeMask"]; + repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"]; + repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::vector"]; + repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"]; + repeated string supported_custom_presets = 17 [(container_pointer) = "std::vector"]; bool disabled_by_default = 18; string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 20; diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index ed49498176..3e9a10c1f7 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1377,16 +1377,16 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { #endif bool supports_current_temperature{false}; bool supports_two_point_target_temperature{false}; - const std::set *supported_modes{}; + const climate::ClimateModeMask *supported_modes{}; float visual_min_temperature{0.0f}; float visual_max_temperature{0.0f}; float visual_target_temperature_step{0.0f}; bool supports_action{false}; - const std::set *supported_fan_modes{}; - const std::set *supported_swing_modes{}; - const std::set *supported_custom_fan_modes{}; - const std::set *supported_presets{}; - const std::set *supported_custom_presets{}; + const climate::ClimateFanModeMask *supported_fan_modes{}; + const climate::ClimateSwingModeMask *supported_swing_modes{}; + const std::vector *supported_custom_fan_modes{}; + const climate::ClimatePresetMask *supported_presets{}; + const std::vector *supported_custom_presets{}; float visual_current_temperature_step{0.0f}; bool supports_current_humidity{false}; bool supports_target_humidity{false}; diff --git a/esphome/components/bedjet/bedjet_const.h b/esphome/components/bedjet/bedjet_const.h index 7cac1b61ff..0693be1092 100644 --- a/esphome/components/bedjet/bedjet_const.h +++ b/esphome/components/bedjet/bedjet_const.h @@ -99,9 +99,8 @@ enum BedjetCommand : uint8_t { static const uint8_t BEDJET_FAN_SPEED_COUNT = 20; -static const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; +static constexpr const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; -static const std::set BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_; } // namespace bedjet } // namespace esphome diff --git a/esphome/components/bedjet/climate/bedjet_climate.h b/esphome/components/bedjet/climate/bedjet_climate.h index 963f2e585a..dbbb73aeae 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.h +++ b/esphome/components/bedjet/climate/bedjet_climate.h @@ -43,7 +43,7 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli }); // It would be better if we had a slider for the fan modes. - traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES_SET); + traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES); traits.set_supported_presets({ // If we support NONE, then have to decide what happens if the user switches to it (turn off?) // climate::CLIMATE_PRESET_NONE, diff --git a/esphome/components/climate/climate_mode_bitmask.h b/esphome/components/climate/climate_mode_bitmask.h index 236d153659..4166829c3f 100644 --- a/esphome/components/climate/climate_mode_bitmask.h +++ b/esphome/components/climate/climate_mode_bitmask.h @@ -9,12 +9,17 @@ namespace climate { // Type aliases for climate enum bitmasks // These replace std::set to eliminate red-black tree overhead -using ClimateModeMask = EnumBitmask; // 7 values (OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO) -using ClimateFanModeMask = - EnumBitmask; // 10 values (ON, OFF, AUTO, LOW, MEDIUM, HIGH, MIDDLE, FOCUS, DIFFUSE, QUIET) -using ClimateSwingModeMask = EnumBitmask; // 4 values (OFF, BOTH, VERTICAL, HORIZONTAL) -using ClimatePresetMask = - EnumBitmask; // 8 values (NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY) +// Bitmask size constants - sized to fit all enum values +constexpr int CLIMATE_MODE_BITMASK_SIZE = 8; // 7 values (OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO) +constexpr int CLIMATE_FAN_MODE_BITMASK_SIZE = + 16; // 10 values (ON, OFF, AUTO, LOW, MEDIUM, HIGH, MIDDLE, FOCUS, DIFFUSE, QUIET) +constexpr int CLIMATE_SWING_MODE_BITMASK_SIZE = 8; // 4 values (OFF, BOTH, VERTICAL, HORIZONTAL) +constexpr int CLIMATE_PRESET_BITMASK_SIZE = 8; // 8 values (NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY) + +using ClimateModeMask = EnumBitmask; +using ClimateFanModeMask = EnumBitmask; +using ClimateSwingModeMask = EnumBitmask; +using ClimatePresetMask = EnumBitmask; } // namespace climate } // namespace esphome @@ -25,12 +30,16 @@ using ClimatePresetMask = namespace esphome { // ClimateMode specialization (7 values: 0-6) -template<> constexpr int EnumBitmask::enum_to_bit(climate::ClimateMode mode) { +template<> +constexpr int EnumBitmask::enum_to_bit( + climate::ClimateMode mode) { return static_cast(mode); // Direct mapping: enum value = bit position } -template<> constexpr climate::ClimateMode EnumBitmask::bit_to_enum(int bit) { - // Compile-time lookup array mapping bit positions to enum values +template<> +inline climate::ClimateMode EnumBitmask::bit_to_enum( + int bit) { + // Lookup array mapping bit positions to enum values static constexpr climate::ClimateMode MODES[] = { climate::CLIMATE_MODE_OFF, // bit 0 climate::CLIMATE_MODE_HEAT_COOL, // bit 1 @@ -40,15 +49,20 @@ template<> constexpr climate::ClimateMode EnumBitmask:: climate::CLIMATE_MODE_DRY, // bit 5 climate::CLIMATE_MODE_AUTO, // bit 6 }; - return (bit >= 0 && bit < 7) ? MODES[bit] : climate::CLIMATE_MODE_OFF; + static constexpr int MODE_COUNT = 7; + return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_MODE_OFF; } // ClimateFanMode specialization (10 values: 0-9) -template<> constexpr int EnumBitmask::enum_to_bit(climate::ClimateFanMode mode) { +template<> +constexpr int EnumBitmask::enum_to_bit( + climate::ClimateFanMode mode) { return static_cast(mode); // Direct mapping: enum value = bit position } -template<> constexpr climate::ClimateFanMode EnumBitmask::bit_to_enum(int bit) { +template<> +inline climate::ClimateFanMode EnumBitmask::bit_to_enum(int bit) { static constexpr climate::ClimateFanMode MODES[] = { climate::CLIMATE_FAN_ON, // bit 0 climate::CLIMATE_FAN_OFF, // bit 1 @@ -61,30 +75,40 @@ template<> constexpr climate::ClimateFanMode EnumBitmask= 0 && bit < 10) ? MODES[bit] : climate::CLIMATE_FAN_ON; + static constexpr int MODE_COUNT = 10; + return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_FAN_ON; } // ClimateSwingMode specialization (4 values: 0-3) -template<> constexpr int EnumBitmask::enum_to_bit(climate::ClimateSwingMode mode) { +template<> +constexpr int EnumBitmask::enum_to_bit( + climate::ClimateSwingMode mode) { return static_cast(mode); // Direct mapping: enum value = bit position } -template<> constexpr climate::ClimateSwingMode EnumBitmask::bit_to_enum(int bit) { +template<> +inline climate::ClimateSwingMode EnumBitmask::bit_to_enum(int bit) { static constexpr climate::ClimateSwingMode MODES[] = { climate::CLIMATE_SWING_OFF, // bit 0 climate::CLIMATE_SWING_BOTH, // bit 1 climate::CLIMATE_SWING_VERTICAL, // bit 2 climate::CLIMATE_SWING_HORIZONTAL, // bit 3 }; - return (bit >= 0 && bit < 4) ? MODES[bit] : climate::CLIMATE_SWING_OFF; + static constexpr int MODE_COUNT = 4; + return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_SWING_OFF; } // ClimatePreset specialization (8 values: 0-7) -template<> constexpr int EnumBitmask::enum_to_bit(climate::ClimatePreset preset) { +template<> +constexpr int EnumBitmask::enum_to_bit( + climate::ClimatePreset preset) { return static_cast(preset); // Direct mapping: enum value = bit position } -template<> constexpr climate::ClimatePreset EnumBitmask::bit_to_enum(int bit) { +template<> +inline climate::ClimatePreset EnumBitmask::bit_to_enum( + int bit) { static constexpr climate::ClimatePreset PRESETS[] = { climate::CLIMATE_PRESET_NONE, // bit 0 climate::CLIMATE_PRESET_HOME, // bit 1 @@ -95,7 +119,8 @@ template<> constexpr climate::ClimatePreset EnumBitmask= 0 && bit < 8) ? PRESETS[bit] : climate::CLIMATE_PRESET_NONE; + static constexpr int PRESET_COUNT = 8; + return (bit >= 0 && bit < PRESET_COUNT) ? PRESETS[bit] : climate::CLIMATE_PRESET_NONE; } } // namespace esphome diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 45287689c9..238c527981 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -150,6 +150,9 @@ class ClimateTraits { void set_supported_custom_fan_modes(std::initializer_list modes) { this->supported_custom_fan_modes_ = modes; } + template void set_supported_custom_fan_modes(const char *const (&modes)[N]) { + this->supported_custom_fan_modes_.assign(modes, modes + N); + } const std::vector &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode); @@ -171,6 +174,9 @@ class ClimateTraits { void set_supported_custom_presets(std::initializer_list presets) { this->supported_custom_presets_ = presets; } + template void set_supported_custom_presets(const char *const (&presets)[N]) { + this->supported_custom_presets_.assign(presets, presets + N); + } const std::vector &get_supported_custom_presets() const { return this->supported_custom_presets_; } bool supports_custom_preset(const std::string &custom_preset) const { return vector_contains(this->supported_custom_presets_, custom_preset); diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index ea0656121f..92eb4a550e 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -3,6 +3,7 @@ #include #include "esphome/components/climate/climate.h" +#include "esphome/components/climate/climate_mode_bitmask.h" #include "esphome/components/remote_base/remote_base.h" #include "esphome/components/remote_transmitter/remote_transmitter.h" #include "esphome/components/sensor/sensor.h" @@ -24,16 +25,18 @@ class ClimateIR : public Component, public remote_base::RemoteTransmittable { public: ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f, - bool supports_dry = false, bool supports_fan_only = false, std::set fan_modes = {}, - std::set swing_modes = {}, std::set presets = {}) { + bool supports_dry = false, bool supports_fan_only = false, + climate::ClimateFanModeMask fan_modes = climate::ClimateFanModeMask(), + climate::ClimateSwingModeMask swing_modes = climate::ClimateSwingModeMask(), + climate::ClimatePresetMask presets = climate::ClimatePresetMask()) { this->minimum_temperature_ = minimum_temperature; this->maximum_temperature_ = maximum_temperature; this->temperature_step_ = temperature_step; this->supports_dry_ = supports_dry; this->supports_fan_only_ = supports_fan_only; - this->fan_modes_ = std::move(fan_modes); - this->swing_modes_ = std::move(swing_modes); - this->presets_ = std::move(presets); + this->fan_modes_ = fan_modes; + this->swing_modes_ = swing_modes; + this->presets_ = presets; } void setup() override; @@ -60,9 +63,9 @@ class ClimateIR : public Component, bool supports_heat_{true}; bool supports_dry_{false}; bool supports_fan_only_{false}; - std::set fan_modes_ = {}; - std::set swing_modes_ = {}; - std::set presets_ = {}; + climate::ClimateFanModeMask fan_modes_{}; + climate::ClimateSwingModeMask swing_modes_{}; + climate::ClimatePresetMask presets_{}; sensor::Sensor *sensor_{nullptr}; }; diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index 5709b8e9b5..1fc971a04e 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -171,26 +171,38 @@ void HaierClimateBase::toggle_power() { PendingAction({ActionRequest::TOGGLE_POWER, esphome::optional()}); } -void HaierClimateBase::set_supported_swing_modes(const std::set &modes) { +void HaierClimateBase::set_supported_swing_modes(climate::ClimateSwingModeMask modes) { this->traits_.set_supported_swing_modes(modes); if (!modes.empty()) this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); } +void HaierClimateBase::set_supported_swing_modes(std::initializer_list modes) { + this->set_supported_swing_modes(climate::ClimateSwingModeMask(modes)); +} + void HaierClimateBase::set_answer_timeout(uint32_t timeout) { this->haier_protocol_.set_answer_timeout(timeout); } -void HaierClimateBase::set_supported_modes(const std::set &modes) { +void HaierClimateBase::set_supported_modes(climate::ClimateModeMask modes) { this->traits_.set_supported_modes(modes); this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF); // Always available this->traits_.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); // Always available } -void HaierClimateBase::set_supported_presets(const std::set &presets) { +void HaierClimateBase::set_supported_modes(std::initializer_list modes) { + this->set_supported_modes(climate::ClimateModeMask(modes)); +} + +void HaierClimateBase::set_supported_presets(climate::ClimatePresetMask presets) { this->traits_.set_supported_presets(presets); if (!presets.empty()) this->traits_.add_supported_preset(climate::CLIMATE_PRESET_NONE); } +void HaierClimateBase::set_supported_presets(std::initializer_list presets) { + this->set_supported_presets(climate::ClimatePresetMask(presets)); +} + void HaierClimateBase::set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; } void HaierClimateBase::send_custom_command(const haier_protocol::HaierMessage &message) { diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h index f0597c49ff..630a5f20e9 100644 --- a/esphome/components/haier/haier_base.h +++ b/esphome/components/haier/haier_base.h @@ -1,8 +1,8 @@ #pragma once #include -#include #include "esphome/components/climate/climate.h" +#include "esphome/components/climate/climate_mode_bitmask.h" #include "esphome/components/uart/uart.h" #include "esphome/core/automation.h" // HaierProtocol @@ -60,9 +60,12 @@ class HaierClimateBase : public esphome::Component, void send_power_off_command(); void toggle_power(); void reset_protocol() { this->reset_protocol_request_ = true; }; - void set_supported_modes(const std::set &modes); - void set_supported_swing_modes(const std::set &modes); - void set_supported_presets(const std::set &presets); + void set_supported_modes(esphome::climate::ClimateModeMask modes); + void set_supported_modes(std::initializer_list modes); + void set_supported_swing_modes(esphome::climate::ClimateSwingModeMask modes); + void set_supported_swing_modes(std::initializer_list modes); + void set_supported_presets(esphome::climate::ClimatePresetMask presets); + void set_supported_presets(std::initializer_list presets); bool valid_connection() const { return this->protocol_phase_ >= ProtocolPhases::IDLE; }; size_t available() noexcept override { return esphome::uart::UARTDevice::available(); }; size_t read_array(uint8_t *data, size_t len) noexcept override { diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index 76558f2ebb..3ab1dab29f 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -1033,9 +1033,9 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * { // Swing mode ClimateSwingMode old_swing_mode = this->swing_mode; - const std::set &swing_modes = traits_.get_supported_swing_modes(); - bool vertical_swing_supported = swing_modes.find(CLIMATE_SWING_VERTICAL) != swing_modes.end(); - bool horizontal_swing_supported = swing_modes.find(CLIMATE_SWING_HORIZONTAL) != swing_modes.end(); + const auto &swing_modes = traits_.get_supported_swing_modes(); + bool vertical_swing_supported = swing_modes.contains(CLIMATE_SWING_VERTICAL); + bool horizontal_swing_supported = swing_modes.contains(CLIMATE_SWING_HORIZONTAL); if (horizontal_swing_supported && (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)) { if (vertical_swing_supported && diff --git a/esphome/components/heatpumpir/heatpumpir.h b/esphome/components/heatpumpir/heatpumpir.h index 3e14c11861..ed43ffdc83 100644 --- a/esphome/components/heatpumpir/heatpumpir.h +++ b/esphome/components/heatpumpir/heatpumpir.h @@ -97,12 +97,11 @@ const float TEMP_MAX = 100; // Celsius class HeatpumpIRClimate : public climate_ir::ClimateIR { public: HeatpumpIRClimate() - : climate_ir::ClimateIR( - TEMP_MIN, TEMP_MAX, 1.0f, true, true, - std::set{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, - climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_AUTO}, - std::set{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL, - climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {} + : climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true, + {climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH, + climate::CLIMATE_FAN_AUTO}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL, + climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {} void setup() override; void set_protocol(Protocol protocol) { this->protocol_ = protocol; } void set_horizontal_default(HorizontalDirection horizontal_direction) { diff --git a/esphome/components/midea/air_conditioner.h b/esphome/components/midea/air_conditioner.h index e70bd34e71..60ee096ec6 100644 --- a/esphome/components/midea/air_conditioner.h +++ b/esphome/components/midea/air_conditioner.h @@ -19,6 +19,9 @@ using climate::ClimateTraits; using climate::ClimateMode; using climate::ClimateSwingMode; using climate::ClimateFanMode; +using climate::ClimateModeMask; +using climate::ClimateSwingModeMask; +using climate::ClimatePresetMask; class AirConditioner : public ApplianceBase, public climate::Climate { public: @@ -40,20 +43,31 @@ class AirConditioner : public ApplianceBase, void do_power_on() { this->base_.setPowerState(true); } void do_power_off() { this->base_.setPowerState(false); } void do_power_toggle() { this->base_.setPowerState(this->mode == ClimateMode::CLIMATE_MODE_OFF); } - void set_supported_modes(const std::set &modes) { this->supported_modes_ = modes; } - void set_supported_swing_modes(const std::set &modes) { this->supported_swing_modes_ = modes; } - void set_supported_presets(const std::set &presets) { this->supported_presets_ = presets; } - void set_custom_presets(const std::set &presets) { this->supported_custom_presets_ = presets; } - void set_custom_fan_modes(const std::set &modes) { this->supported_custom_fan_modes_ = modes; } + void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } + void set_supported_modes(std::initializer_list modes) { + this->supported_modes_ = ClimateModeMask(modes); + } + void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } + void set_supported_swing_modes(std::initializer_list modes) { + this->supported_swing_modes_ = ClimateSwingModeMask(modes); + } + void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } + void set_supported_presets(std::initializer_list presets) { + this->supported_presets_ = ClimatePresetMask(presets); + } + void set_custom_presets(const std::vector &presets) { this->supported_custom_presets_ = presets; } + void set_custom_presets(std::initializer_list presets) { this->supported_custom_presets_ = presets; } + void set_custom_fan_modes(const std::vector &modes) { this->supported_custom_fan_modes_ = modes; } + void set_custom_fan_modes(std::initializer_list modes) { this->supported_custom_fan_modes_ = modes; } protected: void control(const ClimateCall &call) override; ClimateTraits traits() override; - std::set supported_modes_{}; - std::set supported_swing_modes_{}; - std::set supported_presets_{}; - std::set supported_custom_presets_{}; - std::set supported_custom_fan_modes_{}; + ClimateModeMask supported_modes_{}; + ClimateSwingModeMask supported_swing_modes_{}; + ClimatePresetMask supported_presets_{}; + std::vector supported_custom_presets_{}; + std::vector supported_custom_fan_modes_{}; Sensor *outdoor_sensor_{nullptr}; Sensor *humidity_sensor_{nullptr}; Sensor *power_sensor_{nullptr}; diff --git a/esphome/components/toshiba/toshiba.h b/esphome/components/toshiba/toshiba.h index d76833f406..ee1dec5cc9 100644 --- a/esphome/components/toshiba/toshiba.h +++ b/esphome/components/toshiba/toshiba.h @@ -71,10 +71,10 @@ class ToshibaClimate : public climate_ir::ClimateIR { return TOSHIBA_RAS_2819T_TEMP_C_MAX; return TOSHIBA_GENERIC_TEMP_C_MAX; // Default to GENERIC for unknown models } - std::set toshiba_swing_modes_() { + climate::ClimateSwingModeMask toshiba_swing_modes_() { return (this->model_ == MODEL_GENERIC) - ? std::set{} - : std::set{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}; + ? climate::ClimateSwingModeMask() + : climate::ClimateSwingModeMask{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}; } void encode_(remote_base::RemoteTransmitData *data, const uint8_t *message, uint8_t nbytes, uint8_t repeat); bool decode_(remote_base::RemoteReceiveData *data, uint8_t *message, uint8_t nbytes); diff --git a/esphome/components/tuya/climate/tuya_climate.cpp b/esphome/components/tuya/climate/tuya_climate.cpp index 04fb14acff..97de3da353 100644 --- a/esphome/components/tuya/climate/tuya_climate.cpp +++ b/esphome/components/tuya/climate/tuya_climate.cpp @@ -306,18 +306,12 @@ climate::ClimateTraits TuyaClimate::traits() { traits.add_supported_preset(climate::CLIMATE_PRESET_NONE); } if (this->swing_vertical_id_.has_value() && this->swing_horizontal_id_.has_value()) { - std::set supported_swing_modes = { - climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL, - climate::CLIMATE_SWING_HORIZONTAL}; - traits.set_supported_swing_modes(std::move(supported_swing_modes)); + traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, + climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL}); } else if (this->swing_vertical_id_.has_value()) { - std::set supported_swing_modes = {climate::CLIMATE_SWING_OFF, - climate::CLIMATE_SWING_VERTICAL}; - traits.set_supported_swing_modes(std::move(supported_swing_modes)); + traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}); } else if (this->swing_horizontal_id_.has_value()) { - std::set supported_swing_modes = {climate::CLIMATE_SWING_OFF, - climate::CLIMATE_SWING_HORIZONTAL}; - traits.set_supported_swing_modes(std::move(supported_swing_modes)); + traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL}); } if (fan_speed_id_) { diff --git a/esphome/core/enum_bitmask.h b/esphome/core/enum_bitmask.h index 9c208f9efb..4c29c7047e 100644 --- a/esphome/core/enum_bitmask.h +++ b/esphome/core/enum_bitmask.h @@ -147,7 +147,7 @@ template class EnumBitmask { // Must be provided by template specialization // These convert between enum values and bit positions (0, 1, 2, ...) static constexpr int enum_to_bit(EnumType value); - static constexpr EnumType bit_to_enum(int bit); + static EnumType bit_to_enum(int bit); // Not constexpr due to static array limitation in C++20 bitmask_t mask_{0}; }; From bbce28c18da3ea67d021c46ad4c644bf3dd9b8f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 17:21:59 -1000 Subject: [PATCH 004/394] fix compile --- esphome/components/thermostat/thermostat_climate.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 363d2b09fc..42adab7751 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -40,6 +40,10 @@ enum OnBootRestoreFrom : uint8_t { }; struct ThermostatClimateTimer { + ThermostatClimateTimer() = default; + ThermostatClimateTimer(bool active, uint32_t time, uint32_t started, std::function func) + : active(active), time(time), started(started), func(std::move(func)) {} + bool active; uint32_t time; uint32_t started; From f3bf25d203b1af8b2da41d48ec5175b6132517ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 17:25:20 -1000 Subject: [PATCH 005/394] fix compile --- esphome/components/haier/hon_climate.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index 3ab1dab29f..9607343be0 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -1218,13 +1218,13 @@ void HonClimate::fill_control_messages_queue_() { (uint8_t) hon_protocol::DataParameters::QUIET_MODE, quiet_mode_buf, 2); } - if ((fast_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_BOOST) != presets.end()))) { + if ((fast_mode_buf[1] != 0xFF) && presets.contains(climate::ClimatePreset::CLIMATE_PRESET_BOOST)) { this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint8_t) hon_protocol::DataParameters::FAST_MODE, fast_mode_buf, 2); } - if ((away_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_AWAY) != presets.end()))) { + if ((away_mode_buf[1] != 0xFF) && presets.contains(climate::ClimatePreset::CLIMATE_PRESET_AWAY)) { this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint8_t) hon_protocol::DataParameters::TEN_DEGREE, From f7a45783906ea302d6da5cddb7f3ce758360e25f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 17:27:01 -1000 Subject: [PATCH 006/394] fix compile --- esphome/components/climate/climate_mode_bitmask.h | 6 ++---- esphome/components/climate/climate_traits.h | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/esphome/components/climate/climate_mode_bitmask.h b/esphome/components/climate/climate_mode_bitmask.h index 4166829c3f..d04e7b6ec7 100644 --- a/esphome/components/climate/climate_mode_bitmask.h +++ b/esphome/components/climate/climate_mode_bitmask.h @@ -3,8 +3,7 @@ #include "esphome/core/enum_bitmask.h" #include "climate_mode.h" -namespace esphome { -namespace climate { +namespace esphome::climate { // Type aliases for climate enum bitmasks // These replace std::set to eliminate red-black tree overhead @@ -21,8 +20,7 @@ using ClimateFanModeMask = EnumBitmask; using ClimatePresetMask = EnumBitmask; -} // namespace climate -} // namespace esphome +} // namespace esphome::climate // Template specializations for enum-to-bit conversions // All climate enums are sequential starting from 0, so conversions are trivial diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 238c527981..1aef00956b 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -5,8 +5,7 @@ #include "climate_mode_bitmask.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace climate { +namespace esphome::climate { // Lightweight linear search for small vectors (1-20 items) // Avoids std::find template overhead @@ -18,8 +17,7 @@ template inline bool vector_contains(const std::vector &vec, cons return false; } -} // namespace climate -} // namespace esphome +} // namespace esphome::climate namespace esphome { From d3927fe33f7c848b64258a85b3cb33f4653ceb08 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 17:35:24 -1000 Subject: [PATCH 007/394] fix compile --- esphome/components/toshiba/toshiba.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index 36e5a21ffa..5d824b4be8 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -405,7 +405,7 @@ void ToshibaClimate::setup() { this->swing_modes_ = this->toshiba_swing_modes_(); // Ensure swing mode is always initialized to a valid value - if (this->swing_modes_.empty() || this->swing_modes_.find(this->swing_mode) == this->swing_modes_.end()) { + if (this->swing_modes_.empty() || !this->swing_modes_.contains(this->swing_mode)) { // No swing support for this model or current swing mode not supported, reset to OFF this->swing_mode = climate::CLIMATE_SWING_OFF; } From 4dba68589819d870a98909b803c64ddb566d4d49 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:01:39 -1000 Subject: [PATCH 008/394] merge --- esphome/components/light/color_mode.h | 214 +++++++++++++++++------- esphome/components/light/light_call.cpp | 2 +- esphome/components/light/light_traits.h | 2 +- 3 files changed, 156 insertions(+), 62 deletions(-) diff --git a/esphome/components/light/color_mode.h b/esphome/components/light/color_mode.h index 9c6a4d147b..a26f917167 100644 --- a/esphome/components/light/color_mode.h +++ b/esphome/components/light/color_mode.h @@ -1,7 +1,6 @@ #pragma once #include -#include "esphome/core/enum_bitmask.h" namespace esphome { namespace light { @@ -105,16 +104,16 @@ constexpr ColorModeHelper operator|(ColorModeHelper lhs, ColorMode rhs) { return static_cast(static_cast(lhs) | static_cast(rhs)); } -// Type alias for raw color mode bitmask values (retained for compatibility) +// Type alias for raw color mode bitmask values using color_mode_bitmask_t = uint16_t; -// Number of ColorMode enum values -constexpr int COLOR_MODE_BITMASK_SIZE = 10; +// Constants for ColorMode count and bit range +static constexpr int COLOR_MODE_COUNT = 10; // UNKNOWN through RGB_COLD_WARM_WHITE +static constexpr int MAX_BIT_INDEX = sizeof(color_mode_bitmask_t) * 8; // Number of bits in bitmask type -// Shared lookup table for ColorMode bit mapping -// This array defines the canonical order of color modes (bit 0-9) -// Declared early so it can be used by constexpr functions -constexpr ColorMode COLOR_MODE_LOOKUP[COLOR_MODE_BITMASK_SIZE] = { +// Compile-time array of all ColorMode values in declaration order +// Bit positions (0-9) map directly to enum declaration order +static constexpr ColorMode COLOR_MODES[COLOR_MODE_COUNT] = { ColorMode::UNKNOWN, // bit 0 ColorMode::ON_OFF, // bit 1 ColorMode::BRIGHTNESS, // bit 2 @@ -127,20 +126,33 @@ constexpr ColorMode COLOR_MODE_LOOKUP[COLOR_MODE_BITMASK_SIZE] = { ColorMode::RGB_COLD_WARM_WHITE, // bit 9 }; -// Type alias for ColorMode bitmask using generic EnumBitmask template -using ColorModeMask = EnumBitmask; +/// Map ColorMode enum values to bit positions (0-9) +/// Bit positions follow the enum declaration order +static constexpr int mode_to_bit(ColorMode mode) { + // Linear search through COLOR_MODES array + // Compiler optimizes this to efficient code since array is constexpr + for (int i = 0; i < COLOR_MODE_COUNT; ++i) { + if (COLOR_MODES[i] == mode) + return i; + } + return 0; +} -// Number of ColorCapability enum values -constexpr int COLOR_CAPABILITY_COUNT = 6; +/// Map bit positions (0-9) to ColorMode enum values +/// Bit positions follow the enum declaration order +static constexpr ColorMode bit_to_mode(int bit) { + // Direct lookup in COLOR_MODES array + return (bit >= 0 && bit < COLOR_MODE_COUNT) ? COLOR_MODES[bit] : ColorMode::UNKNOWN; +} /// Helper to compute capability bitmask at compile time -constexpr uint16_t compute_capability_bitmask(ColorCapability capability) { - uint16_t mask = 0; +static constexpr color_mode_bitmask_t compute_capability_bitmask(ColorCapability capability) { + color_mode_bitmask_t mask = 0; uint8_t cap_bit = static_cast(capability); // Check each ColorMode to see if it has this capability - for (int bit = 0; bit < COLOR_MODE_BITMASK_SIZE; ++bit) { - uint8_t mode_val = static_cast(COLOR_MODE_LOOKUP[bit]); + for (int bit = 0; bit < COLOR_MODE_COUNT; ++bit) { + uint8_t mode_val = static_cast(bit_to_mode(bit)); if ((mode_val & cap_bit) != 0) { mask |= (1 << bit); } @@ -148,9 +160,12 @@ constexpr uint16_t compute_capability_bitmask(ColorCapability capability) { return mask; } +// Number of ColorCapability enum values +static constexpr int COLOR_CAPABILITY_COUNT = 6; + /// Compile-time lookup table mapping ColorCapability to bitmask /// This array is computed at compile time using constexpr -constexpr uint16_t CAPABILITY_BITMASKS[] = { +static constexpr color_mode_bitmask_t CAPABILITY_BITMASKS[] = { compute_capability_bitmask(ColorCapability::ON_OFF), // 1 << 0 compute_capability_bitmask(ColorCapability::BRIGHTNESS), // 1 << 1 compute_capability_bitmask(ColorCapability::WHITE), // 1 << 2 @@ -159,51 +174,130 @@ constexpr uint16_t CAPABILITY_BITMASKS[] = { compute_capability_bitmask(ColorCapability::RGB), // 1 << 5 }; -/// Check if any mode in the bitmask has a specific capability -/// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB) -inline bool has_capability(const ColorModeMask &mask, ColorCapability capability) { - // Lookup the pre-computed bitmask for this capability and check intersection with our mask - // ColorCapability values: 1, 2, 4, 8, 16, 32 -> array indices: 0, 1, 2, 3, 4, 5 - // We need to convert the power-of-2 value to an index - uint8_t cap_val = static_cast(capability); -#if defined(__GNUC__) || defined(__clang__) - // Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n)) - int index = __builtin_ctz(cap_val); -#else - // Fallback for compilers without __builtin_ctz - int index = 0; - while (cap_val > 1) { - cap_val >>= 1; - ++index; +/// Bitmask for storing a set of ColorMode values efficiently. +/// Replaces std::set to eliminate red-black tree overhead (~586 bytes). +class ColorModeMask { + public: + constexpr ColorModeMask() = default; + + /// Support initializer list syntax: {ColorMode::RGB, ColorMode::WHITE} + constexpr ColorModeMask(std::initializer_list modes) { + for (auto mode : modes) { + this->add(mode); + } } + + constexpr void add(ColorMode mode) { this->mask_ |= (1 << mode_to_bit(mode)); } + + /// Add multiple modes at once using initializer list + constexpr void add(std::initializer_list modes) { + for (auto mode : modes) { + this->add(mode); + } + } + + constexpr bool contains(ColorMode mode) const { return (this->mask_ & (1 << mode_to_bit(mode))) != 0; } + + constexpr size_t size() const { + // Count set bits using Brian Kernighan's algorithm + // More efficient for sparse bitmasks (typical case: 2-4 modes out of 10) + uint16_t n = this->mask_; + size_t count = 0; + while (n) { + n &= n - 1; // Clear the least significant set bit + count++; + } + return count; + } + + constexpr bool empty() const { return this->mask_ == 0; } + + /// Iterator support for API encoding + class Iterator { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = ColorMode; + using difference_type = std::ptrdiff_t; + using pointer = const ColorMode *; + using reference = ColorMode; + + constexpr Iterator(color_mode_bitmask_t mask, int bit) : mask_(mask), bit_(bit) { advance_to_next_set_bit_(); } + + constexpr ColorMode operator*() const { return bit_to_mode(bit_); } + + constexpr Iterator &operator++() { + ++bit_; + advance_to_next_set_bit_(); + return *this; + } + + constexpr bool operator==(const Iterator &other) const { return bit_ == other.bit_; } + + constexpr bool operator!=(const Iterator &other) const { return !(*this == other); } + + private: + constexpr void advance_to_next_set_bit_() { bit_ = ColorModeMask::find_next_set_bit(mask_, bit_); } + + color_mode_bitmask_t mask_; + int bit_; + }; + + constexpr Iterator begin() const { return Iterator(mask_, 0); } + constexpr Iterator end() const { return Iterator(mask_, MAX_BIT_INDEX); } + + /// Get the raw bitmask value for API encoding + constexpr color_mode_bitmask_t get_mask() const { return this->mask_; } + + /// Find the next set bit in a bitmask starting from a given position + /// Returns the bit position, or MAX_BIT_INDEX if no more bits are set + static constexpr int find_next_set_bit(color_mode_bitmask_t mask, int start_bit) { + int bit = start_bit; + while (bit < MAX_BIT_INDEX && !(mask & (1 << bit))) { + ++bit; + } + return bit; + } + + /// Find the first set bit in a bitmask and return the corresponding ColorMode + /// Used for optimizing compute_color_mode_() intersection logic + static constexpr ColorMode first_mode_from_mask(color_mode_bitmask_t mask) { + return bit_to_mode(find_next_set_bit(mask, 0)); + } + + /// Check if a ColorMode is present in a raw bitmask value + /// Useful for checking intersection results without creating a temporary ColorModeMask + static constexpr bool mask_contains(color_mode_bitmask_t mask, ColorMode mode) { + return (mask & (1 << mode_to_bit(mode))) != 0; + } + + /// Check if any mode in the bitmask has a specific capability + /// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB) + bool has_capability(ColorCapability capability) const { + // Lookup the pre-computed bitmask for this capability and check intersection with our mask + // ColorCapability values: 1, 2, 4, 8, 16, 32 -> array indices: 0, 1, 2, 3, 4, 5 + // We need to convert the power-of-2 value to an index + uint8_t cap_val = static_cast(capability); +#if defined(__GNUC__) || defined(__clang__) + // Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n)) + int index = __builtin_ctz(cap_val); +#else + // Fallback for compilers without __builtin_ctz + int index = 0; + while (cap_val > 1) { + cap_val >>= 1; + ++index; + } #endif - return (mask.get_mask() & CAPABILITY_BITMASKS[index]) != 0; -} + return (this->mask_ & CAPABILITY_BITMASKS[index]) != 0; + } + + private: + // Using uint16_t instead of uint32_t for more efficient iteration (fewer bits to scan). + // Currently only 10 ColorMode values exist, so 16 bits is sufficient. + // Can be changed to uint32_t if more than 16 color modes are needed in the future. + // Note: Due to struct padding, uint16_t and uint32_t result in same LightTraits size (12 bytes). + color_mode_bitmask_t mask_{0}; +}; } // namespace light } // namespace esphome - -// Template specializations for ColorMode must be in global namespace - -/// Map ColorMode enum values to bit positions (0-9) -/// Bit positions follow the enum declaration order -template<> -constexpr int esphome::EnumBitmask::enum_to_bit( - esphome::light::ColorMode mode) { - // Linear search through COLOR_MODE_LOOKUP array - // Compiler optimizes this to efficient code since array is constexpr - for (int i = 0; i < esphome::light::COLOR_MODE_BITMASK_SIZE; ++i) { - if (esphome::light::COLOR_MODE_LOOKUP[i] == mode) - return i; - } - return 0; -} - -/// Map bit positions (0-9) to ColorMode enum values -/// Bit positions follow the enum declaration order -template<> -inline esphome::light::ColorMode esphome::EnumBitmask::bit_to_enum(int bit) { - return (bit >= 0 && bit < esphome::light::COLOR_MODE_BITMASK_SIZE) ? esphome::light::COLOR_MODE_LOOKUP[bit] - : esphome::light::ColorMode::UNKNOWN; -} diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 26d14d7bb4..af193e1f11 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -437,7 +437,7 @@ ColorMode LightCall::compute_color_mode_() { // Use the preferred suitable mode. if (intersection != 0) { - ColorMode mode = ColorModeMask::first_value_from_mask(intersection); + ColorMode mode = ColorModeMask::first_mode_from_mask(intersection); ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(mode))); return mode; diff --git a/esphome/components/light/light_traits.h b/esphome/components/light/light_traits.h index 9dec9fb577..4532edca83 100644 --- a/esphome/components/light/light_traits.h +++ b/esphome/components/light/light_traits.h @@ -28,7 +28,7 @@ class LightTraits { bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.contains(color_mode); } bool supports_color_capability(ColorCapability color_capability) const { - return has_capability(this->supported_color_modes_, color_capability); + return this->supported_color_modes_.has_capability(color_capability); } float get_min_mireds() const { return this->min_mireds_; } From 960e6da4f7eb19c655decdc49f7d47d758addb01 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:02:53 -1000 Subject: [PATCH 009/394] [gree] Use EnumBitmask add() instead of insert() for climate traits --- esphome/components/gree/gree.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/gree/gree.cpp b/esphome/components/gree/gree.cpp index e0cacb4f1e..90c5042d69 100644 --- a/esphome/components/gree/gree.cpp +++ b/esphome/components/gree/gree.cpp @@ -8,9 +8,9 @@ static const char *const TAG = "gree.climate"; void GreeClimate::set_model(Model model) { if (model == GREE_YX1FF) { - this->fan_modes_.insert(climate::CLIMATE_FAN_QUIET); // YX1FF 4 speed - this->presets_.insert(climate::CLIMATE_PRESET_NONE); // YX1FF sleep mode - this->presets_.insert(climate::CLIMATE_PRESET_SLEEP); // YX1FF sleep mode + this->fan_modes_.add(climate::CLIMATE_FAN_QUIET); // YX1FF 4 speed + this->presets_.add(climate::CLIMATE_PRESET_NONE); // YX1FF sleep mode + this->presets_.add(climate::CLIMATE_PRESET_SLEEP); // YX1FF sleep mode } this->model_ = model; From 15d4e30df212d13b68a9c173d347481dbe8bfaf6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:04:46 -1000 Subject: [PATCH 010/394] merge --- .../components/climate/climate_mode_bitmask.h | 124 ------------------ esphome/components/climate/climate_traits.h | 118 ++++++++++++++++- esphome/components/climate_ir/climate_ir.h | 1 - esphome/components/haier/haier_base.h | 1 - 4 files changed, 117 insertions(+), 127 deletions(-) delete mode 100644 esphome/components/climate/climate_mode_bitmask.h diff --git a/esphome/components/climate/climate_mode_bitmask.h b/esphome/components/climate/climate_mode_bitmask.h deleted file mode 100644 index d04e7b6ec7..0000000000 --- a/esphome/components/climate/climate_mode_bitmask.h +++ /dev/null @@ -1,124 +0,0 @@ -#pragma once - -#include "esphome/core/enum_bitmask.h" -#include "climate_mode.h" - -namespace esphome::climate { - -// Type aliases for climate enum bitmasks -// These replace std::set to eliminate red-black tree overhead - -// Bitmask size constants - sized to fit all enum values -constexpr int CLIMATE_MODE_BITMASK_SIZE = 8; // 7 values (OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO) -constexpr int CLIMATE_FAN_MODE_BITMASK_SIZE = - 16; // 10 values (ON, OFF, AUTO, LOW, MEDIUM, HIGH, MIDDLE, FOCUS, DIFFUSE, QUIET) -constexpr int CLIMATE_SWING_MODE_BITMASK_SIZE = 8; // 4 values (OFF, BOTH, VERTICAL, HORIZONTAL) -constexpr int CLIMATE_PRESET_BITMASK_SIZE = 8; // 8 values (NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY) - -using ClimateModeMask = EnumBitmask; -using ClimateFanModeMask = EnumBitmask; -using ClimateSwingModeMask = EnumBitmask; -using ClimatePresetMask = EnumBitmask; - -} // namespace esphome::climate - -// Template specializations for enum-to-bit conversions -// All climate enums are sequential starting from 0, so conversions are trivial - -namespace esphome { - -// ClimateMode specialization (7 values: 0-6) -template<> -constexpr int EnumBitmask::enum_to_bit( - climate::ClimateMode mode) { - return static_cast(mode); // Direct mapping: enum value = bit position -} - -template<> -inline climate::ClimateMode EnumBitmask::bit_to_enum( - int bit) { - // Lookup array mapping bit positions to enum values - static constexpr climate::ClimateMode MODES[] = { - climate::CLIMATE_MODE_OFF, // bit 0 - climate::CLIMATE_MODE_HEAT_COOL, // bit 1 - climate::CLIMATE_MODE_COOL, // bit 2 - climate::CLIMATE_MODE_HEAT, // bit 3 - climate::CLIMATE_MODE_FAN_ONLY, // bit 4 - climate::CLIMATE_MODE_DRY, // bit 5 - climate::CLIMATE_MODE_AUTO, // bit 6 - }; - static constexpr int MODE_COUNT = 7; - return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_MODE_OFF; -} - -// ClimateFanMode specialization (10 values: 0-9) -template<> -constexpr int EnumBitmask::enum_to_bit( - climate::ClimateFanMode mode) { - return static_cast(mode); // Direct mapping: enum value = bit position -} - -template<> -inline climate::ClimateFanMode EnumBitmask::bit_to_enum(int bit) { - static constexpr climate::ClimateFanMode MODES[] = { - climate::CLIMATE_FAN_ON, // bit 0 - climate::CLIMATE_FAN_OFF, // bit 1 - climate::CLIMATE_FAN_AUTO, // bit 2 - climate::CLIMATE_FAN_LOW, // bit 3 - climate::CLIMATE_FAN_MEDIUM, // bit 4 - climate::CLIMATE_FAN_HIGH, // bit 5 - climate::CLIMATE_FAN_MIDDLE, // bit 6 - climate::CLIMATE_FAN_FOCUS, // bit 7 - climate::CLIMATE_FAN_DIFFUSE, // bit 8 - climate::CLIMATE_FAN_QUIET, // bit 9 - }; - static constexpr int MODE_COUNT = 10; - return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_FAN_ON; -} - -// ClimateSwingMode specialization (4 values: 0-3) -template<> -constexpr int EnumBitmask::enum_to_bit( - climate::ClimateSwingMode mode) { - return static_cast(mode); // Direct mapping: enum value = bit position -} - -template<> -inline climate::ClimateSwingMode EnumBitmask::bit_to_enum(int bit) { - static constexpr climate::ClimateSwingMode MODES[] = { - climate::CLIMATE_SWING_OFF, // bit 0 - climate::CLIMATE_SWING_BOTH, // bit 1 - climate::CLIMATE_SWING_VERTICAL, // bit 2 - climate::CLIMATE_SWING_HORIZONTAL, // bit 3 - }; - static constexpr int MODE_COUNT = 4; - return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_SWING_OFF; -} - -// ClimatePreset specialization (8 values: 0-7) -template<> -constexpr int EnumBitmask::enum_to_bit( - climate::ClimatePreset preset) { - return static_cast(preset); // Direct mapping: enum value = bit position -} - -template<> -inline climate::ClimatePreset EnumBitmask::bit_to_enum( - int bit) { - static constexpr climate::ClimatePreset PRESETS[] = { - climate::CLIMATE_PRESET_NONE, // bit 0 - climate::CLIMATE_PRESET_HOME, // bit 1 - climate::CLIMATE_PRESET_AWAY, // bit 2 - climate::CLIMATE_PRESET_BOOST, // bit 3 - climate::CLIMATE_PRESET_COMFORT, // bit 4 - climate::CLIMATE_PRESET_ECO, // bit 5 - climate::CLIMATE_PRESET_SLEEP, // bit 6 - climate::CLIMATE_PRESET_ACTIVITY, // bit 7 - }; - static constexpr int PRESET_COUNT = 8; - return (bit >= 0 && bit < PRESET_COUNT) ? PRESETS[bit] : climate::CLIMATE_PRESET_NONE; -} - -} // namespace esphome diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 1aef00956b..8004ba4002 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -2,11 +2,26 @@ #include #include "climate_mode.h" -#include "climate_mode_bitmask.h" +#include "esphome/core/enum_bitmask.h" #include "esphome/core/helpers.h" namespace esphome::climate { +// Type aliases for climate enum bitmasks +// These replace std::set to eliminate red-black tree overhead + +// Bitmask size constants - sized to fit all enum values +constexpr int CLIMATE_MODE_BITMASK_SIZE = 8; // 7 values (OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO) +constexpr int CLIMATE_FAN_MODE_BITMASK_SIZE = + 16; // 10 values (ON, OFF, AUTO, LOW, MEDIUM, HIGH, MIDDLE, FOCUS, DIFFUSE, QUIET) +constexpr int CLIMATE_SWING_MODE_BITMASK_SIZE = 8; // 4 values (OFF, BOTH, VERTICAL, HORIZONTAL) +constexpr int CLIMATE_PRESET_BITMASK_SIZE = 8; // 8 values (NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY) + +using ClimateModeMask = EnumBitmask; +using ClimateFanModeMask = EnumBitmask; +using ClimateSwingModeMask = EnumBitmask; +using ClimatePresetMask = EnumBitmask; + // Lightweight linear search for small vectors (1-20 items) // Avoids std::find template overhead template inline bool vector_contains(const std::vector &vec, const T &value) { @@ -260,3 +275,104 @@ class ClimateTraits { } // namespace climate } // namespace esphome + +// Template specializations for enum-to-bit conversions +// All climate enums are sequential starting from 0, so conversions are trivial + +namespace esphome { + +// ClimateMode specialization (7 values: 0-6) +template<> +constexpr int EnumBitmask::enum_to_bit( + climate::ClimateMode mode) { + return static_cast(mode); // Direct mapping: enum value = bit position +} + +template<> +inline climate::ClimateMode EnumBitmask::bit_to_enum( + int bit) { + // Lookup array mapping bit positions to enum values + static constexpr climate::ClimateMode MODES[] = { + climate::CLIMATE_MODE_OFF, // bit 0 + climate::CLIMATE_MODE_HEAT_COOL, // bit 1 + climate::CLIMATE_MODE_COOL, // bit 2 + climate::CLIMATE_MODE_HEAT, // bit 3 + climate::CLIMATE_MODE_FAN_ONLY, // bit 4 + climate::CLIMATE_MODE_DRY, // bit 5 + climate::CLIMATE_MODE_AUTO, // bit 6 + }; + static constexpr int MODE_COUNT = 7; + return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_MODE_OFF; +} + +// ClimateFanMode specialization (10 values: 0-9) +template<> +constexpr int EnumBitmask::enum_to_bit( + climate::ClimateFanMode mode) { + return static_cast(mode); // Direct mapping: enum value = bit position +} + +template<> +inline climate::ClimateFanMode EnumBitmask::bit_to_enum(int bit) { + static constexpr climate::ClimateFanMode MODES[] = { + climate::CLIMATE_FAN_ON, // bit 0 + climate::CLIMATE_FAN_OFF, // bit 1 + climate::CLIMATE_FAN_AUTO, // bit 2 + climate::CLIMATE_FAN_LOW, // bit 3 + climate::CLIMATE_FAN_MEDIUM, // bit 4 + climate::CLIMATE_FAN_HIGH, // bit 5 + climate::CLIMATE_FAN_MIDDLE, // bit 6 + climate::CLIMATE_FAN_FOCUS, // bit 7 + climate::CLIMATE_FAN_DIFFUSE, // bit 8 + climate::CLIMATE_FAN_QUIET, // bit 9 + }; + static constexpr int MODE_COUNT = 10; + return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_FAN_ON; +} + +// ClimateSwingMode specialization (4 values: 0-3) +template<> +constexpr int EnumBitmask::enum_to_bit( + climate::ClimateSwingMode mode) { + return static_cast(mode); // Direct mapping: enum value = bit position +} + +template<> +inline climate::ClimateSwingMode EnumBitmask::bit_to_enum(int bit) { + static constexpr climate::ClimateSwingMode MODES[] = { + climate::CLIMATE_SWING_OFF, // bit 0 + climate::CLIMATE_SWING_BOTH, // bit 1 + climate::CLIMATE_SWING_VERTICAL, // bit 2 + climate::CLIMATE_SWING_HORIZONTAL, // bit 3 + }; + static constexpr int MODE_COUNT = 4; + return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_SWING_OFF; +} + +// ClimatePreset specialization (8 values: 0-7) +template<> +constexpr int EnumBitmask::enum_to_bit( + climate::ClimatePreset preset) { + return static_cast(preset); // Direct mapping: enum value = bit position +} + +template<> +inline climate::ClimatePreset EnumBitmask::bit_to_enum( + int bit) { + static constexpr climate::ClimatePreset PRESETS[] = { + climate::CLIMATE_PRESET_NONE, // bit 0 + climate::CLIMATE_PRESET_HOME, // bit 1 + climate::CLIMATE_PRESET_AWAY, // bit 2 + climate::CLIMATE_PRESET_BOOST, // bit 3 + climate::CLIMATE_PRESET_COMFORT, // bit 4 + climate::CLIMATE_PRESET_ECO, // bit 5 + climate::CLIMATE_PRESET_SLEEP, // bit 6 + climate::CLIMATE_PRESET_ACTIVITY, // bit 7 + }; + static constexpr int PRESET_COUNT = 8; + return (bit >= 0 && bit < PRESET_COUNT) ? PRESETS[bit] : climate::CLIMATE_PRESET_NONE; +} + +} // namespace esphome diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index 92eb4a550e..62a43f0b2d 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -3,7 +3,6 @@ #include #include "esphome/components/climate/climate.h" -#include "esphome/components/climate/climate_mode_bitmask.h" #include "esphome/components/remote_base/remote_base.h" #include "esphome/components/remote_transmitter/remote_transmitter.h" #include "esphome/components/sensor/sensor.h" diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h index 630a5f20e9..5f57bf6cd0 100644 --- a/esphome/components/haier/haier_base.h +++ b/esphome/components/haier/haier_base.h @@ -2,7 +2,6 @@ #include #include "esphome/components/climate/climate.h" -#include "esphome/components/climate/climate_mode_bitmask.h" #include "esphome/components/uart/uart.h" #include "esphome/core/automation.h" // HaierProtocol From e9e6b9ddf9515a7325e9ccae21ade6d4b82fb2e6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:32:36 -1000 Subject: [PATCH 011/394] minimize changes --- esphome/components/climate/climate_traits.h | 16 ++++++++-------- esphome/components/gree/gree.cpp | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 8004ba4002..21adf5b99c 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -141,17 +141,17 @@ class ClimateTraits { void set_supported_modes(std::initializer_list modes) { this->supported_modes_ = ClimateModeMask(modes); } - void add_supported_mode(ClimateMode mode) { this->supported_modes_.add(mode); } - bool supports_mode(ClimateMode mode) const { return this->supported_modes_.contains(mode); } + void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); } + bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode) > 0; } const ClimateModeMask &get_supported_modes() const { return this->supported_modes_; } void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; } void set_supported_fan_modes(std::initializer_list modes) { this->supported_fan_modes_ = ClimateFanModeMask(modes); } - void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.add(mode); } + void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); } void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.push_back(mode); } - bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.contains(fan_mode); } + bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode) > 0; } bool get_supports_fan_modes() const { return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty(); } @@ -175,9 +175,9 @@ class ClimateTraits { void set_supported_presets(std::initializer_list presets) { this->supported_presets_ = ClimatePresetMask(presets); } - void add_supported_preset(ClimatePreset preset) { this->supported_presets_.add(preset); } + void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); } void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.push_back(preset); } - bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.contains(preset); } + bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset) > 0; } bool get_supports_presets() const { return !this->supported_presets_.empty(); } const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; } @@ -199,9 +199,9 @@ class ClimateTraits { void set_supported_swing_modes(std::initializer_list modes) { this->supported_swing_modes_ = ClimateSwingModeMask(modes); } - void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.add(mode); } + void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); } bool supports_swing_mode(ClimateSwingMode swing_mode) const { - return this->supported_swing_modes_.contains(swing_mode); + return this->supported_swing_modes_.count(swing_mode) > 0; } bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); } const ClimateSwingModeMask &get_supported_swing_modes() const { return this->supported_swing_modes_; } diff --git a/esphome/components/gree/gree.cpp b/esphome/components/gree/gree.cpp index 90c5042d69..e0cacb4f1e 100644 --- a/esphome/components/gree/gree.cpp +++ b/esphome/components/gree/gree.cpp @@ -8,9 +8,9 @@ static const char *const TAG = "gree.climate"; void GreeClimate::set_model(Model model) { if (model == GREE_YX1FF) { - this->fan_modes_.add(climate::CLIMATE_FAN_QUIET); // YX1FF 4 speed - this->presets_.add(climate::CLIMATE_PRESET_NONE); // YX1FF sleep mode - this->presets_.add(climate::CLIMATE_PRESET_SLEEP); // YX1FF sleep mode + this->fan_modes_.insert(climate::CLIMATE_FAN_QUIET); // YX1FF 4 speed + this->presets_.insert(climate::CLIMATE_PRESET_NONE); // YX1FF sleep mode + this->presets_.insert(climate::CLIMATE_PRESET_SLEEP); // YX1FF sleep mode } this->model_ = model; From 2debf04a48f95e3157bd266868ec82f65a38ce04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:32:58 -1000 Subject: [PATCH 012/394] [climate] Use std::set API for EnumBitmask - Change .add() to .insert() - Change .remove() to .erase() - Change .contains() to .count() > 0 - Consistent with std::set API --- esphome/components/climate/climate_traits.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 21adf5b99c..09197a5de3 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -237,23 +237,23 @@ class ClimateTraits { protected: void set_mode_support_(climate::ClimateMode mode, bool supported) { if (supported) { - this->supported_modes_.add(mode); + this->supported_modes_.insert(mode); } else { - this->supported_modes_.remove(mode); + this->supported_modes_.erase(mode); } } void set_fan_mode_support_(climate::ClimateFanMode mode, bool supported) { if (supported) { - this->supported_fan_modes_.add(mode); + this->supported_fan_modes_.insert(mode); } else { - this->supported_fan_modes_.remove(mode); + this->supported_fan_modes_.erase(mode); } } void set_swing_mode_support_(climate::ClimateSwingMode mode, bool supported) { if (supported) { - this->supported_swing_modes_.add(mode); + this->supported_swing_modes_.insert(mode); } else { - this->supported_swing_modes_.remove(mode); + this->supported_swing_modes_.erase(mode); } } From 55d1b823e8e1ba6623b45d7a60c48dc0dc3daa5d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:34:45 -1000 Subject: [PATCH 013/394] minimize changes --- esphome/components/haier/hon_climate.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index 9607343be0..b7ec065261 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -1034,8 +1034,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * // Swing mode ClimateSwingMode old_swing_mode = this->swing_mode; const auto &swing_modes = traits_.get_supported_swing_modes(); - bool vertical_swing_supported = swing_modes.contains(CLIMATE_SWING_VERTICAL); - bool horizontal_swing_supported = swing_modes.contains(CLIMATE_SWING_HORIZONTAL); + bool vertical_swing_supported = swing_modes.count(CLIMATE_SWING_VERTICAL) > 0; + bool horizontal_swing_supported = swing_modes.count(CLIMATE_SWING_HORIZONTAL) > 0; if (horizontal_swing_supported && (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)) { if (vertical_swing_supported && @@ -1218,13 +1218,13 @@ void HonClimate::fill_control_messages_queue_() { (uint8_t) hon_protocol::DataParameters::QUIET_MODE, quiet_mode_buf, 2); } - if ((fast_mode_buf[1] != 0xFF) && presets.contains(climate::ClimatePreset::CLIMATE_PRESET_BOOST)) { + if ((fast_mode_buf[1] != 0xFF) && (presets.count(climate::ClimatePreset::CLIMATE_PRESET_BOOST) > 0)) { this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint8_t) hon_protocol::DataParameters::FAST_MODE, fast_mode_buf, 2); } - if ((away_mode_buf[1] != 0xFF) && presets.contains(climate::ClimatePreset::CLIMATE_PRESET_AWAY)) { + if ((away_mode_buf[1] != 0xFF) && (presets.count(climate::ClimatePreset::CLIMATE_PRESET_AWAY) > 0)) { this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint8_t) hon_protocol::DataParameters::TEN_DEGREE, From d8e8c2832ea7c5a1972ef6e2ed9bc35922c906ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:34:58 -1000 Subject: [PATCH 014/394] minimize changes --- esphome/components/toshiba/toshiba.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index 5d824b4be8..ef96caf238 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -405,7 +405,7 @@ void ToshibaClimate::setup() { this->swing_modes_ = this->toshiba_swing_modes_(); // Ensure swing mode is always initialized to a valid value - if (this->swing_modes_.empty() || !this->swing_modes_.contains(this->swing_mode)) { + if (this->swing_modes_.empty() || (this->swing_modes_.count(this->swing_mode) == 0)) { // No swing support for this model or current swing mode not supported, reset to OFF this->swing_mode = climate::CLIMATE_SWING_OFF; } From 1eca67bb4c379cecba123ea509bcbacf3a7c3032 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:36:33 -1000 Subject: [PATCH 015/394] [climate] Remove redundant initializer_list overloads EnumBitmask already has a constructor that takes initializer_list, so the explicit overloads are unnecessary and add code duplication. --- esphome/components/climate/climate_traits.h | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 09197a5de3..5ee6b40230 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -138,17 +138,11 @@ class ClimateTraits { } void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } - void set_supported_modes(std::initializer_list modes) { - this->supported_modes_ = ClimateModeMask(modes); - } void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); } bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode) > 0; } const ClimateModeMask &get_supported_modes() const { return this->supported_modes_; } void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; } - void set_supported_fan_modes(std::initializer_list modes) { - this->supported_fan_modes_ = ClimateFanModeMask(modes); - } void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); } void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.push_back(mode); } bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode) > 0; } @@ -172,9 +166,6 @@ class ClimateTraits { } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } - void set_supported_presets(std::initializer_list presets) { - this->supported_presets_ = ClimatePresetMask(presets); - } void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); } void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.push_back(preset); } bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset) > 0; } @@ -196,9 +187,6 @@ class ClimateTraits { } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } - void set_supported_swing_modes(std::initializer_list modes) { - this->supported_swing_modes_ = ClimateSwingModeMask(modes); - } void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); } bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode) > 0; From 0ad42ec79bb351e34b04109b2b1e6de8d56859ba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:37:19 -1000 Subject: [PATCH 016/394] minimize changes --- esphome/components/haier/haier_base.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h index 5f57bf6cd0..e24217bfd9 100644 --- a/esphome/components/haier/haier_base.h +++ b/esphome/components/haier/haier_base.h @@ -60,11 +60,8 @@ class HaierClimateBase : public esphome::Component, void toggle_power(); void reset_protocol() { this->reset_protocol_request_ = true; }; void set_supported_modes(esphome::climate::ClimateModeMask modes); - void set_supported_modes(std::initializer_list modes); void set_supported_swing_modes(esphome::climate::ClimateSwingModeMask modes); - void set_supported_swing_modes(std::initializer_list modes); void set_supported_presets(esphome::climate::ClimatePresetMask presets); - void set_supported_presets(std::initializer_list presets); bool valid_connection() const { return this->protocol_phase_ >= ProtocolPhases::IDLE; }; size_t available() noexcept override { return esphome::uart::UARTDevice::available(); }; size_t read_array(uint8_t *data, size_t len) noexcept override { From 0d256e12a6ea805e3067e141e6f00bc84ae9065f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:37:48 -1000 Subject: [PATCH 017/394] [climate] Remove redundant initializer_list overloads from haier and midea EnumBitmask and std::vector already handle initializer_list via implicit conversion, so explicit overloads are unnecessary. --- esphome/components/haier/haier_base.cpp | 12 ------------ esphome/components/midea/air_conditioner.h | 11 ----------- 2 files changed, 23 deletions(-) diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index 1fc971a04e..cd2673a272 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -177,10 +177,6 @@ void HaierClimateBase::set_supported_swing_modes(climate::ClimateSwingModeMask m this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); } -void HaierClimateBase::set_supported_swing_modes(std::initializer_list modes) { - this->set_supported_swing_modes(climate::ClimateSwingModeMask(modes)); -} - void HaierClimateBase::set_answer_timeout(uint32_t timeout) { this->haier_protocol_.set_answer_timeout(timeout); } void HaierClimateBase::set_supported_modes(climate::ClimateModeMask modes) { @@ -189,20 +185,12 @@ void HaierClimateBase::set_supported_modes(climate::ClimateModeMask modes) { this->traits_.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); // Always available } -void HaierClimateBase::set_supported_modes(std::initializer_list modes) { - this->set_supported_modes(climate::ClimateModeMask(modes)); -} - void HaierClimateBase::set_supported_presets(climate::ClimatePresetMask presets) { this->traits_.set_supported_presets(presets); if (!presets.empty()) this->traits_.add_supported_preset(climate::CLIMATE_PRESET_NONE); } -void HaierClimateBase::set_supported_presets(std::initializer_list presets) { - this->set_supported_presets(climate::ClimatePresetMask(presets)); -} - void HaierClimateBase::set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; } void HaierClimateBase::send_custom_command(const haier_protocol::HaierMessage &message) { diff --git a/esphome/components/midea/air_conditioner.h b/esphome/components/midea/air_conditioner.h index 60ee096ec6..6c2401efe7 100644 --- a/esphome/components/midea/air_conditioner.h +++ b/esphome/components/midea/air_conditioner.h @@ -44,21 +44,10 @@ class AirConditioner : public ApplianceBase, void do_power_off() { this->base_.setPowerState(false); } void do_power_toggle() { this->base_.setPowerState(this->mode == ClimateMode::CLIMATE_MODE_OFF); } void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } - void set_supported_modes(std::initializer_list modes) { - this->supported_modes_ = ClimateModeMask(modes); - } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } - void set_supported_swing_modes(std::initializer_list modes) { - this->supported_swing_modes_ = ClimateSwingModeMask(modes); - } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } - void set_supported_presets(std::initializer_list presets) { - this->supported_presets_ = ClimatePresetMask(presets); - } void set_custom_presets(const std::vector &presets) { this->supported_custom_presets_ = presets; } - void set_custom_presets(std::initializer_list presets) { this->supported_custom_presets_ = presets; } void set_custom_fan_modes(const std::vector &modes) { this->supported_custom_fan_modes_ = modes; } - void set_custom_fan_modes(std::initializer_list modes) { this->supported_custom_fan_modes_ = modes; } protected: void control(const ClimateCall &call) override; From ae1af5f16e18b4e769973d1029abf8eae7970c29 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:38:44 -1000 Subject: [PATCH 018/394] minimize changes --- esphome/components/climate/climate_traits.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 5ee6b40230..7cf4a307e3 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -139,13 +139,13 @@ class ClimateTraits { void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); } - bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode) > 0; } + bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); } const ClimateModeMask &get_supported_modes() const { return this->supported_modes_; } void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; } void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); } void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.push_back(mode); } - bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode) > 0; } + bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); } bool get_supports_fan_modes() const { return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty(); } @@ -168,7 +168,7 @@ class ClimateTraits { void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); } void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.push_back(preset); } - bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset) > 0; } + bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); } bool get_supports_presets() const { return !this->supported_presets_.empty(); } const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; } From 7310d7557985167380dbfaadc48c1aa042cf5477 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:39:11 -1000 Subject: [PATCH 019/394] minimize changes --- esphome/components/climate/climate_traits.h | 4 +--- esphome/components/haier/hon_climate.cpp | 8 ++++---- esphome/components/toshiba/toshiba.cpp | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 7cf4a307e3..f84133aa2a 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -188,9 +188,7 @@ class ClimateTraits { void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); } - bool supports_swing_mode(ClimateSwingMode swing_mode) const { - return this->supported_swing_modes_.count(swing_mode) > 0; - } + bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); } bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); } const ClimateSwingModeMask &get_supported_swing_modes() const { return this->supported_swing_modes_; } diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index b7ec065261..23d28bfd47 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -1034,8 +1034,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * // Swing mode ClimateSwingMode old_swing_mode = this->swing_mode; const auto &swing_modes = traits_.get_supported_swing_modes(); - bool vertical_swing_supported = swing_modes.count(CLIMATE_SWING_VERTICAL) > 0; - bool horizontal_swing_supported = swing_modes.count(CLIMATE_SWING_HORIZONTAL) > 0; + bool vertical_swing_supported = swing_modes.count(CLIMATE_SWING_VERTICAL); + bool horizontal_swing_supported = swing_modes.count(CLIMATE_SWING_HORIZONTAL); if (horizontal_swing_supported && (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)) { if (vertical_swing_supported && @@ -1218,13 +1218,13 @@ void HonClimate::fill_control_messages_queue_() { (uint8_t) hon_protocol::DataParameters::QUIET_MODE, quiet_mode_buf, 2); } - if ((fast_mode_buf[1] != 0xFF) && (presets.count(climate::ClimatePreset::CLIMATE_PRESET_BOOST) > 0)) { + if ((fast_mode_buf[1] != 0xFF) && presets.count(climate::ClimatePreset::CLIMATE_PRESET_BOOST)) { this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint8_t) hon_protocol::DataParameters::FAST_MODE, fast_mode_buf, 2); } - if ((away_mode_buf[1] != 0xFF) && (presets.count(climate::ClimatePreset::CLIMATE_PRESET_AWAY) > 0)) { + if ((away_mode_buf[1] != 0xFF) && presets.count(climate::ClimatePreset::CLIMATE_PRESET_AWAY)) { this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint8_t) hon_protocol::DataParameters::TEN_DEGREE, diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index ef96caf238..5efa70d6b4 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -405,7 +405,7 @@ void ToshibaClimate::setup() { this->swing_modes_ = this->toshiba_swing_modes_(); // Ensure swing mode is always initialized to a valid value - if (this->swing_modes_.empty() || (this->swing_modes_.count(this->swing_mode) == 0)) { + if (this->swing_modes_.empty() || !this->swing_modes_.count(this->swing_mode)) { // No swing support for this model or current swing mode not supported, reset to OFF this->swing_mode = climate::CLIMATE_SWING_OFF; } From 44c24100179b67b46f0d5eeff80b99d30fba052e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:48:42 -1000 Subject: [PATCH 020/394] preen --- esphome/components/climate/climate_traits.h | 213 ++++++++++---------- 1 file changed, 107 insertions(+), 106 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index f84133aa2a..bdb04a65cc 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -5,18 +5,120 @@ #include "esphome/core/enum_bitmask.h" #include "esphome/core/helpers.h" +// Forward declare climate enums and bitmask sizes namespace esphome::climate { - -// Type aliases for climate enum bitmasks -// These replace std::set to eliminate red-black tree overhead - -// Bitmask size constants - sized to fit all enum values constexpr int CLIMATE_MODE_BITMASK_SIZE = 8; // 7 values (OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO) constexpr int CLIMATE_FAN_MODE_BITMASK_SIZE = 16; // 10 values (ON, OFF, AUTO, LOW, MEDIUM, HIGH, MIDDLE, FOCUS, DIFFUSE, QUIET) constexpr int CLIMATE_SWING_MODE_BITMASK_SIZE = 8; // 4 values (OFF, BOTH, VERTICAL, HORIZONTAL) constexpr int CLIMATE_PRESET_BITMASK_SIZE = 8; // 8 values (NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY) +} // namespace esphome::climate +// Template specializations for enum-to-bit conversions +// MUST be declared before any instantiation of EnumBitmask, etc. +namespace esphome { + +// ClimateMode specialization (7 values: 0-6) +template<> +constexpr int EnumBitmask::enum_to_bit( + climate::ClimateMode mode) { + return static_cast(mode); // Direct mapping: enum value = bit position +} + +template<> +inline climate::ClimateMode EnumBitmask::bit_to_enum( + int bit) { + // Lookup array mapping bit positions to enum values + static constexpr climate::ClimateMode MODES[] = { + climate::CLIMATE_MODE_OFF, // bit 0 + climate::CLIMATE_MODE_HEAT_COOL, // bit 1 + climate::CLIMATE_MODE_COOL, // bit 2 + climate::CLIMATE_MODE_HEAT, // bit 3 + climate::CLIMATE_MODE_FAN_ONLY, // bit 4 + climate::CLIMATE_MODE_DRY, // bit 5 + climate::CLIMATE_MODE_AUTO, // bit 6 + }; + static constexpr int MODE_COUNT = 7; + return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_MODE_OFF; +} + +// ClimateFanMode specialization (10 values: 0-9) +template<> +constexpr int EnumBitmask::enum_to_bit( + climate::ClimateFanMode mode) { + return static_cast(mode); // Direct mapping: enum value = bit position +} + +template<> +inline climate::ClimateFanMode EnumBitmask::bit_to_enum(int bit) { + static constexpr climate::ClimateFanMode MODES[] = { + climate::CLIMATE_FAN_ON, // bit 0 + climate::CLIMATE_FAN_OFF, // bit 1 + climate::CLIMATE_FAN_AUTO, // bit 2 + climate::CLIMATE_FAN_LOW, // bit 3 + climate::CLIMATE_FAN_MEDIUM, // bit 4 + climate::CLIMATE_FAN_HIGH, // bit 5 + climate::CLIMATE_FAN_MIDDLE, // bit 6 + climate::CLIMATE_FAN_FOCUS, // bit 7 + climate::CLIMATE_FAN_DIFFUSE, // bit 8 + climate::CLIMATE_FAN_QUIET, // bit 9 + }; + static constexpr int MODE_COUNT = 10; + return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_FAN_ON; +} + +// ClimateSwingMode specialization (4 values: 0-3) +template<> +constexpr int EnumBitmask::enum_to_bit( + climate::ClimateSwingMode mode) { + return static_cast(mode); // Direct mapping: enum value = bit position +} + +template<> +inline climate::ClimateSwingMode EnumBitmask::bit_to_enum(int bit) { + static constexpr climate::ClimateSwingMode MODES[] = { + climate::CLIMATE_SWING_OFF, // bit 0 + climate::CLIMATE_SWING_BOTH, // bit 1 + climate::CLIMATE_SWING_VERTICAL, // bit 2 + climate::CLIMATE_SWING_HORIZONTAL, // bit 3 + }; + static constexpr int MODE_COUNT = 4; + return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_SWING_OFF; +} + +// ClimatePreset specialization (8 values: 0-7) +template<> +constexpr int EnumBitmask::enum_to_bit( + climate::ClimatePreset preset) { + return static_cast(preset); // Direct mapping: enum value = bit position +} + +template<> +inline climate::ClimatePreset EnumBitmask::bit_to_enum( + int bit) { + static constexpr climate::ClimatePreset PRESETS[] = { + climate::CLIMATE_PRESET_NONE, // bit 0 + climate::CLIMATE_PRESET_HOME, // bit 1 + climate::CLIMATE_PRESET_AWAY, // bit 2 + climate::CLIMATE_PRESET_BOOST, // bit 3 + climate::CLIMATE_PRESET_COMFORT, // bit 4 + climate::CLIMATE_PRESET_ECO, // bit 5 + climate::CLIMATE_PRESET_SLEEP, // bit 6 + climate::CLIMATE_PRESET_ACTIVITY, // bit 7 + }; + static constexpr int PRESET_COUNT = 8; + return (bit >= 0 && bit < PRESET_COUNT) ? PRESETS[bit] : climate::CLIMATE_PRESET_NONE; +} + +} // namespace esphome + +// Now we can safely create the type aliases +namespace esphome::climate { + +// Type aliases for climate enum bitmasks +// These replace std::set to eliminate red-black tree overhead using ClimateModeMask = EnumBitmask; using ClimateFanModeMask = EnumBitmask; using ClimateSwingModeMask = EnumBitmask; @@ -261,104 +363,3 @@ class ClimateTraits { } // namespace climate } // namespace esphome - -// Template specializations for enum-to-bit conversions -// All climate enums are sequential starting from 0, so conversions are trivial - -namespace esphome { - -// ClimateMode specialization (7 values: 0-6) -template<> -constexpr int EnumBitmask::enum_to_bit( - climate::ClimateMode mode) { - return static_cast(mode); // Direct mapping: enum value = bit position -} - -template<> -inline climate::ClimateMode EnumBitmask::bit_to_enum( - int bit) { - // Lookup array mapping bit positions to enum values - static constexpr climate::ClimateMode MODES[] = { - climate::CLIMATE_MODE_OFF, // bit 0 - climate::CLIMATE_MODE_HEAT_COOL, // bit 1 - climate::CLIMATE_MODE_COOL, // bit 2 - climate::CLIMATE_MODE_HEAT, // bit 3 - climate::CLIMATE_MODE_FAN_ONLY, // bit 4 - climate::CLIMATE_MODE_DRY, // bit 5 - climate::CLIMATE_MODE_AUTO, // bit 6 - }; - static constexpr int MODE_COUNT = 7; - return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_MODE_OFF; -} - -// ClimateFanMode specialization (10 values: 0-9) -template<> -constexpr int EnumBitmask::enum_to_bit( - climate::ClimateFanMode mode) { - return static_cast(mode); // Direct mapping: enum value = bit position -} - -template<> -inline climate::ClimateFanMode EnumBitmask::bit_to_enum(int bit) { - static constexpr climate::ClimateFanMode MODES[] = { - climate::CLIMATE_FAN_ON, // bit 0 - climate::CLIMATE_FAN_OFF, // bit 1 - climate::CLIMATE_FAN_AUTO, // bit 2 - climate::CLIMATE_FAN_LOW, // bit 3 - climate::CLIMATE_FAN_MEDIUM, // bit 4 - climate::CLIMATE_FAN_HIGH, // bit 5 - climate::CLIMATE_FAN_MIDDLE, // bit 6 - climate::CLIMATE_FAN_FOCUS, // bit 7 - climate::CLIMATE_FAN_DIFFUSE, // bit 8 - climate::CLIMATE_FAN_QUIET, // bit 9 - }; - static constexpr int MODE_COUNT = 10; - return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_FAN_ON; -} - -// ClimateSwingMode specialization (4 values: 0-3) -template<> -constexpr int EnumBitmask::enum_to_bit( - climate::ClimateSwingMode mode) { - return static_cast(mode); // Direct mapping: enum value = bit position -} - -template<> -inline climate::ClimateSwingMode EnumBitmask::bit_to_enum(int bit) { - static constexpr climate::ClimateSwingMode MODES[] = { - climate::CLIMATE_SWING_OFF, // bit 0 - climate::CLIMATE_SWING_BOTH, // bit 1 - climate::CLIMATE_SWING_VERTICAL, // bit 2 - climate::CLIMATE_SWING_HORIZONTAL, // bit 3 - }; - static constexpr int MODE_COUNT = 4; - return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_SWING_OFF; -} - -// ClimatePreset specialization (8 values: 0-7) -template<> -constexpr int EnumBitmask::enum_to_bit( - climate::ClimatePreset preset) { - return static_cast(preset); // Direct mapping: enum value = bit position -} - -template<> -inline climate::ClimatePreset EnumBitmask::bit_to_enum( - int bit) { - static constexpr climate::ClimatePreset PRESETS[] = { - climate::CLIMATE_PRESET_NONE, // bit 0 - climate::CLIMATE_PRESET_HOME, // bit 1 - climate::CLIMATE_PRESET_AWAY, // bit 2 - climate::CLIMATE_PRESET_BOOST, // bit 3 - climate::CLIMATE_PRESET_COMFORT, // bit 4 - climate::CLIMATE_PRESET_ECO, // bit 5 - climate::CLIMATE_PRESET_SLEEP, // bit 6 - climate::CLIMATE_PRESET_ACTIVITY, // bit 7 - }; - static constexpr int PRESET_COUNT = 8; - return (bit >= 0 && bit < PRESET_COUNT) ? PRESETS[bit] : climate::CLIMATE_PRESET_NONE; -} - -} // namespace esphome From 35afa7ae059b1475ce94a106a2a68db3c1640976 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 08:52:27 -1000 Subject: [PATCH 021/394] migrate --- esphome/components/climate/climate_traits.h | 34 ++++++++++----------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index bdb04a65cc..9bff36f69f 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -2,7 +2,7 @@ #include #include "climate_mode.h" -#include "esphome/core/enum_bitmask.h" +#include "esphome/core/finite_set_mask.h" #include "esphome/core/helpers.h" // Forward declare climate enums and bitmask sizes @@ -14,19 +14,19 @@ constexpr int CLIMATE_SWING_MODE_BITMASK_SIZE = 8; // 4 values (OFF, BOTH, VERT constexpr int CLIMATE_PRESET_BITMASK_SIZE = 8; // 8 values (NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY) } // namespace esphome::climate -// Template specializations for enum-to-bit conversions -// MUST be declared before any instantiation of EnumBitmask, etc. +// Template specializations for value-to-bit conversions +// MUST be declared before any instantiation of FiniteSetMask, etc. namespace esphome { // ClimateMode specialization (7 values: 0-6) template<> -constexpr int EnumBitmask::enum_to_bit( +constexpr int FiniteSetMask::value_to_bit( climate::ClimateMode mode) { return static_cast(mode); // Direct mapping: enum value = bit position } template<> -inline climate::ClimateMode EnumBitmask::bit_to_enum( +inline climate::ClimateMode FiniteSetMask::bit_to_value( int bit) { // Lookup array mapping bit positions to enum values static constexpr climate::ClimateMode MODES[] = { @@ -44,14 +44,14 @@ inline climate::ClimateMode EnumBitmask -constexpr int EnumBitmask::enum_to_bit( +constexpr int FiniteSetMask::value_to_bit( climate::ClimateFanMode mode) { return static_cast(mode); // Direct mapping: enum value = bit position } template<> -inline climate::ClimateFanMode EnumBitmask::bit_to_enum(int bit) { +inline climate::ClimateFanMode FiniteSetMask::bit_to_value(int bit) { static constexpr climate::ClimateFanMode MODES[] = { climate::CLIMATE_FAN_ON, // bit 0 climate::CLIMATE_FAN_OFF, // bit 1 @@ -70,14 +70,14 @@ inline climate::ClimateFanMode EnumBitmask -constexpr int EnumBitmask::enum_to_bit( +constexpr int FiniteSetMask::value_to_bit( climate::ClimateSwingMode mode) { return static_cast(mode); // Direct mapping: enum value = bit position } template<> -inline climate::ClimateSwingMode EnumBitmask::bit_to_enum(int bit) { +inline climate::ClimateSwingMode FiniteSetMask::bit_to_value(int bit) { static constexpr climate::ClimateSwingMode MODES[] = { climate::CLIMATE_SWING_OFF, // bit 0 climate::CLIMATE_SWING_BOTH, // bit 1 @@ -90,13 +90,13 @@ inline climate::ClimateSwingMode EnumBitmask -constexpr int EnumBitmask::enum_to_bit( +constexpr int FiniteSetMask::value_to_bit( climate::ClimatePreset preset) { return static_cast(preset); // Direct mapping: enum value = bit position } template<> -inline climate::ClimatePreset EnumBitmask::bit_to_enum( +inline climate::ClimatePreset FiniteSetMask::bit_to_value( int bit) { static constexpr climate::ClimatePreset PRESETS[] = { climate::CLIMATE_PRESET_NONE, // bit 0 @@ -119,10 +119,10 @@ namespace esphome::climate { // Type aliases for climate enum bitmasks // These replace std::set to eliminate red-black tree overhead -using ClimateModeMask = EnumBitmask; -using ClimateFanModeMask = EnumBitmask; -using ClimateSwingModeMask = EnumBitmask; -using ClimatePresetMask = EnumBitmask; +using ClimateModeMask = FiniteSetMask; +using ClimateFanModeMask = FiniteSetMask; +using ClimateSwingModeMask = FiniteSetMask; +using ClimatePresetMask = FiniteSetMask; // Lightweight linear search for small vectors (1-20 items) // Avoids std::find template overhead From d7f32bf27f24cd62656a75469bb0271e1a49c84b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 09:44:14 -1000 Subject: [PATCH 022/394] reduce --- esphome/components/climate/climate_traits.h | 36 +++++---------------- esphome/core/finite_set_mask.h | 26 +++++++++++---- 2 files changed, 28 insertions(+), 34 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 9bff36f69f..b90ef963a7 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -18,13 +18,8 @@ constexpr int CLIMATE_PRESET_BITMASK_SIZE = 8; // 8 values (NONE, HOME, AWA // MUST be declared before any instantiation of FiniteSetMask, etc. namespace esphome { -// ClimateMode specialization (7 values: 0-6) -template<> -constexpr int FiniteSetMask::value_to_bit( - climate::ClimateMode mode) { - return static_cast(mode); // Direct mapping: enum value = bit position -} - +// ClimateMode uses 1:1 mapping (value_to_bit is just a cast) +// Only bit_to_value needs specialization template<> inline climate::ClimateMode FiniteSetMask::bit_to_value( int bit) { @@ -42,13 +37,8 @@ inline climate::ClimateMode FiniteSetMask= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_MODE_OFF; } -// ClimateFanMode specialization (10 values: 0-9) -template<> -constexpr int FiniteSetMask::value_to_bit( - climate::ClimateFanMode mode) { - return static_cast(mode); // Direct mapping: enum value = bit position -} - +// ClimateFanMode uses 1:1 mapping (value_to_bit is just a cast) +// Only bit_to_value needs specialization template<> inline climate::ClimateFanMode FiniteSetMask::bit_to_value(int bit) { @@ -68,13 +58,8 @@ inline climate::ClimateFanMode FiniteSetMask= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_FAN_ON; } -// ClimateSwingMode specialization (4 values: 0-3) -template<> -constexpr int FiniteSetMask::value_to_bit( - climate::ClimateSwingMode mode) { - return static_cast(mode); // Direct mapping: enum value = bit position -} - +// ClimateSwingMode uses 1:1 mapping (value_to_bit is just a cast) +// Only bit_to_value needs specialization template<> inline climate::ClimateSwingMode FiniteSetMask::bit_to_value(int bit) { @@ -88,13 +73,8 @@ inline climate::ClimateSwingMode FiniteSetMask= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_SWING_OFF; } -// ClimatePreset specialization (8 values: 0-7) -template<> -constexpr int FiniteSetMask::value_to_bit( - climate::ClimatePreset preset) { - return static_cast(preset); // Direct mapping: enum value = bit position -} - +// ClimatePreset uses 1:1 mapping (value_to_bit is just a cast) +// Only bit_to_value needs specialization template<> inline climate::ClimatePreset FiniteSetMask::bit_to_value( int bit) { diff --git a/esphome/core/finite_set_mask.h b/esphome/core/finite_set_mask.h index e6e7564d4b..ebf134960b 100644 --- a/esphome/core/finite_set_mask.h +++ b/esphome/core/finite_set_mask.h @@ -17,17 +17,27 @@ namespace esphome { /// /// Requirements: /// - ValueType must have a bounded discrete range that maps to bit positions -/// - Specialization must provide value_to_bit() and bit_to_value() static methods +/// - Specialization must provide bit_to_value() static method +/// - For 1:1 mappings (enum value = bit position), default value_to_bit() is used +/// - For custom mappings (like ColorMode), specialize value_to_bit() as well /// - MaxBits must be sufficient to hold all possible values /// -/// Example usage: +/// Example usage (1:1 mapping - climate enums): +/// // For enums with contiguous values starting at 0, only bit_to_value() needs specialization +/// template<> +/// inline ClimateMode FiniteSetMask::bit_to_value(int bit) { +/// static constexpr ClimateMode MODES[] = {CLIMATE_MODE_OFF, CLIMATE_MODE_HEAT, ...}; +/// return (bit >= 0 && bit < 7) ? MODES[bit] : CLIMATE_MODE_OFF; +/// } +/// /// using ClimateModeMask = FiniteSetMask; /// ClimateModeMask modes({CLIMATE_MODE_HEAT, CLIMATE_MODE_COOL}); /// if (modes.count(CLIMATE_MODE_HEAT)) { ... } /// for (auto mode : modes) { ... } // Iterate over set bits /// -/// For complete usage examples with template specializations, see: -/// - esphome/components/light/color_mode.h (ColorMode enum example) +/// Example usage (custom mapping - ColorMode): +/// // For custom mappings, specialize both value_to_bit() and bit_to_value() +/// // See esphome/components/light/color_mode.h for complete example /// /// Design notes: /// - Uses compile-time type selection for optimal size (uint8_t/uint16_t/uint32_t) @@ -150,9 +160,13 @@ template class FiniteSetMask { } protected: + // Default implementation for 1:1 mapping (enum value = bit position) + // For enums with contiguous values starting at 0, this is all you need. + // If you need custom mapping (like ColorMode), provide a specialization. + static constexpr int value_to_bit(ValueType value) { return static_cast(value); } + // Must be provided by template specialization - // These convert between values and bit positions (0, 1, 2, ...) - static constexpr int value_to_bit(ValueType value); + // Converts bit positions (0, 1, 2, ...) to actual values static ValueType bit_to_value(int bit); // Not constexpr: array indexing with runtime bounds checking bitmask_t mask_{0}; From ce80baa3c92a7e329332a45f1d5b073b3599b749 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 09:46:13 -1000 Subject: [PATCH 023/394] reduce --- esphome/components/climate/climate_traits.h | 83 +-------------------- esphome/core/finite_set_mask.h | 26 ++----- 2 files changed, 10 insertions(+), 99 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index b90ef963a7..4def5044ca 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -14,87 +14,8 @@ constexpr int CLIMATE_SWING_MODE_BITMASK_SIZE = 8; // 4 values (OFF, BOTH, VERT constexpr int CLIMATE_PRESET_BITMASK_SIZE = 8; // 8 values (NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY) } // namespace esphome::climate -// Template specializations for value-to-bit conversions -// MUST be declared before any instantiation of FiniteSetMask, etc. -namespace esphome { - -// ClimateMode uses 1:1 mapping (value_to_bit is just a cast) -// Only bit_to_value needs specialization -template<> -inline climate::ClimateMode FiniteSetMask::bit_to_value( - int bit) { - // Lookup array mapping bit positions to enum values - static constexpr climate::ClimateMode MODES[] = { - climate::CLIMATE_MODE_OFF, // bit 0 - climate::CLIMATE_MODE_HEAT_COOL, // bit 1 - climate::CLIMATE_MODE_COOL, // bit 2 - climate::CLIMATE_MODE_HEAT, // bit 3 - climate::CLIMATE_MODE_FAN_ONLY, // bit 4 - climate::CLIMATE_MODE_DRY, // bit 5 - climate::CLIMATE_MODE_AUTO, // bit 6 - }; - static constexpr int MODE_COUNT = 7; - return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_MODE_OFF; -} - -// ClimateFanMode uses 1:1 mapping (value_to_bit is just a cast) -// Only bit_to_value needs specialization -template<> -inline climate::ClimateFanMode FiniteSetMask::bit_to_value(int bit) { - static constexpr climate::ClimateFanMode MODES[] = { - climate::CLIMATE_FAN_ON, // bit 0 - climate::CLIMATE_FAN_OFF, // bit 1 - climate::CLIMATE_FAN_AUTO, // bit 2 - climate::CLIMATE_FAN_LOW, // bit 3 - climate::CLIMATE_FAN_MEDIUM, // bit 4 - climate::CLIMATE_FAN_HIGH, // bit 5 - climate::CLIMATE_FAN_MIDDLE, // bit 6 - climate::CLIMATE_FAN_FOCUS, // bit 7 - climate::CLIMATE_FAN_DIFFUSE, // bit 8 - climate::CLIMATE_FAN_QUIET, // bit 9 - }; - static constexpr int MODE_COUNT = 10; - return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_FAN_ON; -} - -// ClimateSwingMode uses 1:1 mapping (value_to_bit is just a cast) -// Only bit_to_value needs specialization -template<> -inline climate::ClimateSwingMode FiniteSetMask::bit_to_value(int bit) { - static constexpr climate::ClimateSwingMode MODES[] = { - climate::CLIMATE_SWING_OFF, // bit 0 - climate::CLIMATE_SWING_BOTH, // bit 1 - climate::CLIMATE_SWING_VERTICAL, // bit 2 - climate::CLIMATE_SWING_HORIZONTAL, // bit 3 - }; - static constexpr int MODE_COUNT = 4; - return (bit >= 0 && bit < MODE_COUNT) ? MODES[bit] : climate::CLIMATE_SWING_OFF; -} - -// ClimatePreset uses 1:1 mapping (value_to_bit is just a cast) -// Only bit_to_value needs specialization -template<> -inline climate::ClimatePreset FiniteSetMask::bit_to_value( - int bit) { - static constexpr climate::ClimatePreset PRESETS[] = { - climate::CLIMATE_PRESET_NONE, // bit 0 - climate::CLIMATE_PRESET_HOME, // bit 1 - climate::CLIMATE_PRESET_AWAY, // bit 2 - climate::CLIMATE_PRESET_BOOST, // bit 3 - climate::CLIMATE_PRESET_COMFORT, // bit 4 - climate::CLIMATE_PRESET_ECO, // bit 5 - climate::CLIMATE_PRESET_SLEEP, // bit 6 - climate::CLIMATE_PRESET_ACTIVITY, // bit 7 - }; - static constexpr int PRESET_COUNT = 8; - return (bit >= 0 && bit < PRESET_COUNT) ? PRESETS[bit] : climate::CLIMATE_PRESET_NONE; -} - -} // namespace esphome - -// Now we can safely create the type aliases +// No template specializations needed - all climate enums use 1:1 mapping (enum value = bit position) +// FiniteSetMask's default implementations handle this automatically. namespace esphome::climate { // Type aliases for climate enum bitmasks diff --git a/esphome/core/finite_set_mask.h b/esphome/core/finite_set_mask.h index ebf134960b..fdb9bcbc08 100644 --- a/esphome/core/finite_set_mask.h +++ b/esphome/core/finite_set_mask.h @@ -17,26 +17,19 @@ namespace esphome { /// /// Requirements: /// - ValueType must have a bounded discrete range that maps to bit positions -/// - Specialization must provide bit_to_value() static method -/// - For 1:1 mappings (enum value = bit position), default value_to_bit() is used -/// - For custom mappings (like ColorMode), specialize value_to_bit() as well +/// - For 1:1 mappings (contiguous enums starting at 0), no specialization needed +/// - For custom mappings (like ColorMode), specialize value_to_bit() and/or bit_to_value() /// - MaxBits must be sufficient to hold all possible values /// /// Example usage (1:1 mapping - climate enums): -/// // For enums with contiguous values starting at 0, only bit_to_value() needs specialization -/// template<> -/// inline ClimateMode FiniteSetMask::bit_to_value(int bit) { -/// static constexpr ClimateMode MODES[] = {CLIMATE_MODE_OFF, CLIMATE_MODE_HEAT, ...}; -/// return (bit >= 0 && bit < 7) ? MODES[bit] : CLIMATE_MODE_OFF; -/// } -/// +/// // For enums with contiguous values starting at 0, no specialization needed! /// using ClimateModeMask = FiniteSetMask; /// ClimateModeMask modes({CLIMATE_MODE_HEAT, CLIMATE_MODE_COOL}); /// if (modes.count(CLIMATE_MODE_HEAT)) { ... } /// for (auto mode : modes) { ... } // Iterate over set bits /// /// Example usage (custom mapping - ColorMode): -/// // For custom mappings, specialize both value_to_bit() and bit_to_value() +/// // For non-contiguous enums or custom mappings, specialize value_to_bit() and/or bit_to_value() /// // See esphome/components/light/color_mode.h for complete example /// /// Design notes: @@ -160,14 +153,11 @@ template class FiniteSetMask { } protected: - // Default implementation for 1:1 mapping (enum value = bit position) - // For enums with contiguous values starting at 0, this is all you need. - // If you need custom mapping (like ColorMode), provide a specialization. + // Default implementations for 1:1 mapping (enum value = bit position) + // For enums with contiguous values starting at 0, these defaults work as-is. + // If you need custom mapping (like ColorMode), provide specializations. static constexpr int value_to_bit(ValueType value) { return static_cast(value); } - - // Must be provided by template specialization - // Converts bit positions (0, 1, 2, ...) to actual values - static ValueType bit_to_value(int bit); // Not constexpr: array indexing with runtime bounds checking + static constexpr ValueType bit_to_value(int bit) { return static_cast(bit); } bitmask_t mask_{0}; }; From 56d084bcffcaad385d6e99fde2eeb739ffaed039 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 09:47:31 -1000 Subject: [PATCH 024/394] reduce --- esphome/components/climate/climate_traits.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 4def5044ca..d0855d58b1 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -5,18 +5,17 @@ #include "esphome/core/finite_set_mask.h" #include "esphome/core/helpers.h" -// Forward declare climate enums and bitmask sizes namespace esphome::climate { + +// Bitmask sizes for climate enums constexpr int CLIMATE_MODE_BITMASK_SIZE = 8; // 7 values (OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO) constexpr int CLIMATE_FAN_MODE_BITMASK_SIZE = 16; // 10 values (ON, OFF, AUTO, LOW, MEDIUM, HIGH, MIDDLE, FOCUS, DIFFUSE, QUIET) constexpr int CLIMATE_SWING_MODE_BITMASK_SIZE = 8; // 4 values (OFF, BOTH, VERTICAL, HORIZONTAL) constexpr int CLIMATE_PRESET_BITMASK_SIZE = 8; // 8 values (NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY) -} // namespace esphome::climate // No template specializations needed - all climate enums use 1:1 mapping (enum value = bit position) // FiniteSetMask's default implementations handle this automatically. -namespace esphome::climate { // Type aliases for climate enum bitmasks // These replace std::set to eliminate red-black tree overhead From 73944d4077886bde010641665edd33599d0f6aaa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 09:48:39 -1000 Subject: [PATCH 025/394] reduce --- esphome/components/climate/climate_traits.h | 22 +++++++++------------ 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index d0855d58b1..42affba3e9 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -5,7 +5,15 @@ #include "esphome/core/finite_set_mask.h" #include "esphome/core/helpers.h" -namespace esphome::climate { +namespace esphome { + +#ifdef USE_API +namespace api { +class APIConnection; +} // namespace api +#endif + +namespace climate { // Bitmask sizes for climate enums constexpr int CLIMATE_MODE_BITMASK_SIZE = 8; // 7 values (OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO) @@ -34,18 +42,6 @@ template inline bool vector_contains(const std::vector &vec, cons return false; } -} // namespace esphome::climate - -namespace esphome { - -#ifdef USE_API -namespace api { -class APIConnection; -} // namespace api -#endif - -namespace climate { - /** This class contains all static data for climate devices. * * All climate devices must support these features: From 8e9a438c4679decfe48da861df176cd4f6fbe375 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 09:51:15 -1000 Subject: [PATCH 026/394] reduce --- esphome/components/climate/climate_mode.h | 12 ++++++++---- esphome/components/climate/climate_traits.h | 19 +++++-------------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index faec5d2537..44423d2f22 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -7,6 +7,7 @@ namespace esphome { namespace climate { /// Enum for all modes a climate device can be in. +/// NOTE: If adding values, update ClimateModeMask in climate_traits.h to use the new last value enum ClimateMode : uint8_t { /// The climate device is off CLIMATE_MODE_OFF = 0, @@ -24,7 +25,7 @@ enum ClimateMode : uint8_t { * For example, the target temperature can be adjusted based on a schedule, or learned behavior. * The target temperature can't be adjusted when in this mode. */ - CLIMATE_MODE_AUTO = 6 + CLIMATE_MODE_AUTO = 6 // Update ClimateModeMask in climate_traits.h if adding values after this }; /// Enum for the current action of the climate device. Values match those of ClimateMode. @@ -43,6 +44,7 @@ enum ClimateAction : uint8_t { CLIMATE_ACTION_FAN = 6, }; +/// NOTE: If adding values, update ClimateFanModeMask in climate_traits.h to use the new last value enum ClimateFanMode : uint8_t { /// The fan mode is set to On CLIMATE_FAN_ON = 0, @@ -63,10 +65,11 @@ enum ClimateFanMode : uint8_t { /// The fan mode is set to Diffuse CLIMATE_FAN_DIFFUSE = 8, /// The fan mode is set to Quiet - CLIMATE_FAN_QUIET = 9, + CLIMATE_FAN_QUIET = 9, // Update ClimateFanModeMask in climate_traits.h if adding values after this }; /// Enum for all modes a climate swing can be in +/// NOTE: If adding values, update ClimateSwingModeMask in climate_traits.h to use the new last value enum ClimateSwingMode : uint8_t { /// The swing mode is set to Off CLIMATE_SWING_OFF = 0, @@ -75,10 +78,11 @@ enum ClimateSwingMode : uint8_t { /// The fan mode is set to Vertical CLIMATE_SWING_VERTICAL = 2, /// The fan mode is set to Horizontal - CLIMATE_SWING_HORIZONTAL = 3, + CLIMATE_SWING_HORIZONTAL = 3, // Update ClimateSwingModeMask in climate_traits.h if adding values after this }; /// Enum for all preset modes +/// NOTE: If adding values, update ClimatePresetMask in climate_traits.h to use the new last value enum ClimatePreset : uint8_t { /// No preset is active CLIMATE_PRESET_NONE = 0, @@ -95,7 +99,7 @@ enum ClimatePreset : uint8_t { /// Device is prepared for sleep CLIMATE_PRESET_SLEEP = 6, /// Device is reacting to activity (e.g., movement sensors) - CLIMATE_PRESET_ACTIVITY = 7, + CLIMATE_PRESET_ACTIVITY = 7, // Update ClimatePresetMask in climate_traits.h if adding values after this }; enum ClimateFeature : uint32_t { diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 42affba3e9..cddd10e47a 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -15,22 +15,13 @@ class APIConnection; namespace climate { -// Bitmask sizes for climate enums -constexpr int CLIMATE_MODE_BITMASK_SIZE = 8; // 7 values (OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO) -constexpr int CLIMATE_FAN_MODE_BITMASK_SIZE = - 16; // 10 values (ON, OFF, AUTO, LOW, MEDIUM, HIGH, MIDDLE, FOCUS, DIFFUSE, QUIET) -constexpr int CLIMATE_SWING_MODE_BITMASK_SIZE = 8; // 4 values (OFF, BOTH, VERTICAL, HORIZONTAL) -constexpr int CLIMATE_PRESET_BITMASK_SIZE = 8; // 8 values (NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY) - -// No template specializations needed - all climate enums use 1:1 mapping (enum value = bit position) -// FiniteSetMask's default implementations handle this automatically. - // Type aliases for climate enum bitmasks // These replace std::set to eliminate red-black tree overhead -using ClimateModeMask = FiniteSetMask; -using ClimateFanModeMask = FiniteSetMask; -using ClimateSwingModeMask = FiniteSetMask; -using ClimatePresetMask = FiniteSetMask; +// For contiguous enums starting at 0, bitmask size is automatically calculated from the last enum value +using ClimateModeMask = FiniteSetMask; +using ClimateFanModeMask = FiniteSetMask; +using ClimateSwingModeMask = FiniteSetMask; +using ClimatePresetMask = FiniteSetMask; // Lightweight linear search for small vectors (1-20 items) // Avoids std::find template overhead From 7c7f1e755dafe4c9459897e6a35ed0e49d835205 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 09:55:10 -1000 Subject: [PATCH 027/394] merge --- esphome/core/finite_set_mask.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/finite_set_mask.h b/esphome/core/finite_set_mask.h index fdb9bcbc08..ab2454508f 100644 --- a/esphome/core/finite_set_mask.h +++ b/esphome/core/finite_set_mask.h @@ -23,7 +23,7 @@ namespace esphome { /// /// Example usage (1:1 mapping - climate enums): /// // For enums with contiguous values starting at 0, no specialization needed! -/// using ClimateModeMask = FiniteSetMask; +/// using ClimateModeMask = FiniteSetMask; /// ClimateModeMask modes({CLIMATE_MODE_HEAT, CLIMATE_MODE_COOL}); /// if (modes.count(CLIMATE_MODE_HEAT)) { ... } /// for (auto mode : modes) { ... } // Iterate over set bits From 94809c4687511ce06964b3933fe0fade8347ea56 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 10:07:36 -1000 Subject: [PATCH 028/394] merge --- esphome/core/finite_set_mask.h | 70 ++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/esphome/core/finite_set_mask.h b/esphome/core/finite_set_mask.h index ab2454508f..d3f0b52a71 100644 --- a/esphome/core/finite_set_mask.h +++ b/esphome/core/finite_set_mask.h @@ -8,44 +8,54 @@ namespace esphome { +/// Default bit mapping policy for contiguous enums starting at 0 +/// Provides 1:1 mapping where enum value equals bit position +template struct DefaultBitPolicy { + // Automatic bitmask type selection based on MaxBits + // ≤8 bits: uint8_t, ≤16 bits: uint16_t, otherwise: uint32_t + using mask_t = typename std::conditional<(MaxBits <= 8), uint8_t, + typename std::conditional<(MaxBits <= 16), uint16_t, uint32_t>::type>::type; + + static constexpr int max_bits = MaxBits; + + static constexpr unsigned to_bit(ValueType value) { return static_cast(value); } + + static constexpr ValueType from_bit(unsigned bit) { return static_cast(bit); } +}; + /// Generic bitmask for storing a finite set of discrete values efficiently. /// Replaces std::set to eliminate red-black tree overhead (~586 bytes per instantiation). /// /// Template parameters: /// ValueType: The type to store (typically enum, but can be any discrete bounded type) -/// MaxBits: Maximum number of bits needed (auto-selects uint8_t/uint16_t/uint32_t) +/// BitPolicy: Policy class defining bit mapping and mask type (defaults to DefaultBitPolicy) /// -/// Requirements: -/// - ValueType must have a bounded discrete range that maps to bit positions -/// - For 1:1 mappings (contiguous enums starting at 0), no specialization needed -/// - For custom mappings (like ColorMode), specialize value_to_bit() and/or bit_to_value() -/// - MaxBits must be sufficient to hold all possible values +/// BitPolicy requirements: +/// - using mask_t = // Bitmask storage type +/// - static constexpr int max_bits // Maximum number of bits +/// - static constexpr unsigned to_bit(ValueType) // Convert value to bit position +/// - static constexpr ValueType from_bit(unsigned) // Convert bit position to value /// /// Example usage (1:1 mapping - climate enums): -/// // For enums with contiguous values starting at 0, no specialization needed! -/// using ClimateModeMask = FiniteSetMask; +/// // For contiguous enums starting at 0, use DefaultBitPolicy +/// using ClimateModeMask = FiniteSetMask>; /// ClimateModeMask modes({CLIMATE_MODE_HEAT, CLIMATE_MODE_COOL}); /// if (modes.count(CLIMATE_MODE_HEAT)) { ... } -/// for (auto mode : modes) { ... } // Iterate over set bits +/// for (auto mode : modes) { ... } /// /// Example usage (custom mapping - ColorMode): -/// // For non-contiguous enums or custom mappings, specialize value_to_bit() and/or bit_to_value() +/// // For custom mappings, define a custom BitPolicy /// // See esphome/components/light/color_mode.h for complete example /// /// Design notes: -/// - Uses compile-time type selection for optimal size (uint8_t/uint16_t/uint32_t) +/// - Policy-based design allows custom bit mappings without template specialization /// - Iterator converts bit positions to actual values during traversal /// - All operations are constexpr-compatible for compile-time initialization /// - Drop-in replacement for std::set with simpler API -/// - Despite the name, works with any discrete bounded type, not just enums /// -template class FiniteSetMask { +template> class FiniteSetMask { public: - // Automatic bitmask type selection based on MaxBits - // ≤8 bits: uint8_t, ≤16 bits: uint16_t, otherwise: uint32_t - using bitmask_t = - typename std::conditional<(MaxBits <= 8), uint8_t, - typename std::conditional<(MaxBits <= 16), uint16_t, uint32_t>::type>::type; + using bitmask_t = typename BitPolicy::mask_t; constexpr FiniteSetMask() = default; @@ -57,7 +67,7 @@ template class FiniteSetMask { } /// Add a single value to the set (std::set compatibility) - constexpr void insert(ValueType value) { this->mask_ |= (static_cast(1) << value_to_bit(value)); } + constexpr void insert(ValueType value) { this->mask_ |= (static_cast(1) << BitPolicy::to_bit(value)); } /// Add multiple values from initializer list constexpr void insert(std::initializer_list values) { @@ -67,7 +77,7 @@ template class FiniteSetMask { } /// Remove a value from the set (std::set compatibility) - constexpr void erase(ValueType value) { this->mask_ &= ~(static_cast(1) << value_to_bit(value)); } + constexpr void erase(ValueType value) { this->mask_ &= ~(static_cast(1) << BitPolicy::to_bit(value)); } /// Clear all values from the set constexpr void clear() { this->mask_ = 0; } @@ -75,7 +85,7 @@ template class FiniteSetMask { /// Check if the set contains a specific value (std::set compatibility) /// Returns 1 if present, 0 if not (same as std::set for unique elements) constexpr size_t count(ValueType value) const { - return (this->mask_ & (static_cast(1) << value_to_bit(value))) != 0 ? 1 : 0; + return (this->mask_ & (static_cast(1) << BitPolicy::to_bit(value))) != 0 ? 1 : 0; } /// Count the number of values in the set @@ -109,7 +119,7 @@ template class FiniteSetMask { constexpr ValueType operator*() const { // Return value for the first set bit - return bit_to_value(find_next_set_bit(mask_, 0)); + return BitPolicy::from_bit(find_next_set_bit(mask_, 0)); } constexpr Iterator &operator++() { @@ -135,30 +145,26 @@ template class FiniteSetMask { /// Check if a specific value is present in a raw bitmask /// Useful for checking intersection results without creating temporary objects static constexpr bool mask_contains(bitmask_t mask, ValueType value) { - return (mask & (static_cast(1) << value_to_bit(value))) != 0; + return (mask & (static_cast(1) << BitPolicy::to_bit(value))) != 0; } /// Get the first value from a raw bitmask /// Used for optimizing intersection logic (e.g., "pick first suitable mode") - static constexpr ValueType first_value_from_mask(bitmask_t mask) { return bit_to_value(find_next_set_bit(mask, 0)); } + static constexpr ValueType first_value_from_mask(bitmask_t mask) { + return BitPolicy::from_bit(find_next_set_bit(mask, 0)); + } /// Find the next set bit in a bitmask starting from a given position - /// Returns the bit position, or MaxBits if no more bits are set + /// Returns the bit position, or max_bits if no more bits are set static constexpr int find_next_set_bit(bitmask_t mask, int start_bit) { int bit = start_bit; - while (bit < MaxBits && !(mask & (static_cast(1) << bit))) { + while (bit < BitPolicy::max_bits && !(mask & (static_cast(1) << bit))) { ++bit; } return bit; } protected: - // Default implementations for 1:1 mapping (enum value = bit position) - // For enums with contiguous values starting at 0, these defaults work as-is. - // If you need custom mapping (like ColorMode), provide specializations. - static constexpr int value_to_bit(ValueType value) { return static_cast(value); } - static constexpr ValueType bit_to_value(int bit) { return static_cast(bit); } - bitmask_t mask_{0}; }; From a284a06916df260c74e8a7bcacc2fdfd46bff3a4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 10:08:27 -1000 Subject: [PATCH 029/394] policy --- esphome/components/climate/climate_traits.h | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index cddd10e47a..97fb4d0432 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -17,11 +17,13 @@ namespace climate { // Type aliases for climate enum bitmasks // These replace std::set to eliminate red-black tree overhead -// For contiguous enums starting at 0, bitmask size is automatically calculated from the last enum value -using ClimateModeMask = FiniteSetMask; -using ClimateFanModeMask = FiniteSetMask; -using ClimateSwingModeMask = FiniteSetMask; -using ClimatePresetMask = FiniteSetMask; +// For contiguous enums starting at 0, DefaultBitPolicy provides 1:1 mapping (enum value = bit position) +// Bitmask size is automatically calculated from the last enum value +using ClimateModeMask = FiniteSetMask>; +using ClimateFanModeMask = FiniteSetMask>; +using ClimateSwingModeMask = + FiniteSetMask>; +using ClimatePresetMask = FiniteSetMask>; // Lightweight linear search for small vectors (1-20 items) // Avoids std::find template overhead From 42a86fe3330b0f6daac8ea52311963927510a1fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 10:18:51 -1000 Subject: [PATCH 030/394] merge --- esphome/core/finite_set_mask.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/core/finite_set_mask.h b/esphome/core/finite_set_mask.h index d3f0b52a71..f9cd0377c7 100644 --- a/esphome/core/finite_set_mask.h +++ b/esphome/core/finite_set_mask.h @@ -16,7 +16,7 @@ template struct DefaultBitPolicy { using mask_t = typename std::conditional<(MaxBits <= 8), uint8_t, typename std::conditional<(MaxBits <= 16), uint16_t, uint32_t>::type>::type; - static constexpr int max_bits = MaxBits; + static constexpr int MAX_BITS = MaxBits; static constexpr unsigned to_bit(ValueType value) { return static_cast(value); } @@ -32,7 +32,7 @@ template struct DefaultBitPolicy { /// /// BitPolicy requirements: /// - using mask_t = // Bitmask storage type -/// - static constexpr int max_bits // Maximum number of bits +/// - static constexpr int MAX_BITS // Maximum number of bits /// - static constexpr unsigned to_bit(ValueType) // Convert value to bit position /// - static constexpr ValueType from_bit(unsigned) // Convert bit position to value /// @@ -155,10 +155,10 @@ template(1) << bit))) { + while (bit < BitPolicy::MAX_BITS && !(mask & (static_cast(1) << bit))) { ++bit; } return bit; From f58b90a67c31496cb07bb41b5c8a2d133bb13fe6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 10:34:44 -1000 Subject: [PATCH 031/394] preen --- esphome/components/climate/climate_traits.h | 7 ------- 1 file changed, 7 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 97fb4d0432..1161a54f4e 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -6,13 +6,6 @@ #include "esphome/core/helpers.h" namespace esphome { - -#ifdef USE_API -namespace api { -class APIConnection; -} // namespace api -#endif - namespace climate { // Type aliases for climate enum bitmasks From b91b12d77a28236644970051d576c0c4b284b564 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Thu, 23 Oct 2025 02:55:34 +0200 Subject: [PATCH 032/394] [nrf52] support BLE --device for logging (#9861) Co-authored-by: J. Nick Koston --- esphome/components/nrf52/__init__.py | 17 ++++++++ esphome/components/nrf52/ble_logger.py | 60 ++++++++++++++++++++++++++ esphome/components/zephyr/__init__.py | 3 ++ requirements.txt | 1 + 4 files changed, 81 insertions(+) create mode 100644 esphome/components/nrf52/ble_logger.py diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 727607933d..27e1246744 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio import logging from pathlib import Path @@ -277,3 +278,19 @@ def upload_program(config: ConfigType, args, host: str) -> bool: raise EsphomeError(f"Upload failed with result: {result}") return handled + + +def show_logs(config: ConfigType, args, devices: list[str]) -> bool: + address = devices[0] + from .ble_logger import is_mac_address, logger_connect, logger_scan + + if devices[0] == "BLE": + ble_device = asyncio.run(logger_scan(CORE.config["esphome"]["name"])) + if ble_device: + address = ble_device.address + else: + return True + if is_mac_address(address): + asyncio.run(logger_connect(address)) + return True + return False diff --git a/esphome/components/nrf52/ble_logger.py b/esphome/components/nrf52/ble_logger.py new file mode 100644 index 0000000000..f74a49ea89 --- /dev/null +++ b/esphome/components/nrf52/ble_logger.py @@ -0,0 +1,60 @@ +import asyncio +import logging +import re +from typing import Final + +from bleak import BleakClient, BleakScanner, BLEDevice +from bleak.exc import ( + BleakCharacteristicNotFoundError, + BleakDBusError, + BleakDeviceNotFoundError, +) + +_LOGGER = logging.getLogger(__name__) + + +NUS_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" +NUS_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" + +MAC_ADDRESS_PATTERN: Final = re.compile( + r"([0-9A-F]{2}[:]){5}[0-9A-F]{2}$", flags=re.IGNORECASE +) + + +def is_mac_address(value: str) -> bool: + return MAC_ADDRESS_PATTERN.match(value) + + +async def logger_scan(name: str) -> BLEDevice | None: + _LOGGER.info("Scanning bluetooth for %s...", name) + device = await BleakScanner.find_device_by_name(name) + if not device: + _LOGGER.error("%s Bluetooth LE device was not found!", name) + return device + + +async def logger_connect(host: str) -> int | None: + disconnected_event = asyncio.Event() + + def handle_disconnect(client): + disconnected_event.set() + + def handle_rx(_, data: bytearray): + print(data.decode("utf-8"), end="") + + _LOGGER.info("Connecting %s...", host) + try: + async with BleakClient(host, disconnected_callback=handle_disconnect) as client: + _LOGGER.info("Connected %s...", host) + try: + await client.start_notify(NUS_TX_CHAR_UUID, handle_rx) + except BleakDBusError as e: + _LOGGER.error("Bluetooth LE logger: %s", e) + disconnected_event.set() + await disconnected_event.wait() + except BleakDeviceNotFoundError: + _LOGGER.error("Device %s not found", host) + return 1 + except BleakCharacteristicNotFoundError: + _LOGGER.error("Device %s has no NUS characteristic", host) + return 1 diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index 634c99876b..a2fb12a5e2 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -234,6 +234,9 @@ def copy_files(): "url": "https://esphome.io/", "vendor": "esphome", "build": { + "bsp": { + "name": "adafruit" + }, "softdevice": { "sd_fwid": "0x00B6" } diff --git a/requirements.txt b/requirements.txt index ec7794c75a..6966ebe583 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,6 +22,7 @@ pillow==11.3.0 cairosvg==2.8.2 freetype-py==2.5.1 jinja2==3.1.6 +bleak==1.0.1 # esp-idf >= 5.0 requires this pyparsing >= 3.0 From f2f6c597ef3768a37507acb5c4831ad7e858cf6f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 15:17:57 -1000 Subject: [PATCH 033/394] [light] Store effect names in flash (const char*) to save RAM (#11487) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../adalight/adalight_light_effect.cpp | 2 +- .../adalight/adalight_light_effect.h | 2 +- esphome/components/e131/e131.cpp | 8 ++++---- .../e131/e131_addressable_light_effect.cpp | 6 +++--- .../e131/e131_addressable_light_effect.h | 2 +- .../light/addressable_light_effect.h | 19 +++++++++---------- esphome/components/light/base_light_effects.h | 12 ++++++------ esphome/components/light/light_call.cpp | 4 ++-- esphome/components/light/light_effect.h | 12 +++++++----- esphome/components/light/light_state.h | 2 +- esphome/components/wled/wled_light_effect.cpp | 2 +- esphome/components/wled/wled_light_effect.h | 2 +- 12 files changed, 37 insertions(+), 36 deletions(-) diff --git a/esphome/components/adalight/adalight_light_effect.cpp b/esphome/components/adalight/adalight_light_effect.cpp index 35e98d7360..4cf639a01f 100644 --- a/esphome/components/adalight/adalight_light_effect.cpp +++ b/esphome/components/adalight/adalight_light_effect.cpp @@ -9,7 +9,7 @@ static const char *const TAG = "adalight_light_effect"; static const uint32_t ADALIGHT_ACK_INTERVAL = 1000; static const uint32_t ADALIGHT_RECEIVE_TIMEOUT = 1000; -AdalightLightEffect::AdalightLightEffect(const std::string &name) : AddressableLightEffect(name) {} +AdalightLightEffect::AdalightLightEffect(const char *name) : AddressableLightEffect(name) {} void AdalightLightEffect::start() { AddressableLightEffect::start(); diff --git a/esphome/components/adalight/adalight_light_effect.h b/esphome/components/adalight/adalight_light_effect.h index 72faf44269..bb7319c99c 100644 --- a/esphome/components/adalight/adalight_light_effect.h +++ b/esphome/components/adalight/adalight_light_effect.h @@ -11,7 +11,7 @@ namespace adalight { class AdalightLightEffect : public light::AddressableLightEffect, public uart::UARTDevice { public: - AdalightLightEffect(const std::string &name); + AdalightLightEffect(const char *name); void start() override; void stop() override; diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index a74fc9be4a..d18d945cec 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -80,8 +80,8 @@ void E131Component::add_effect(E131AddressableLightEffect *light_effect) { return; } - ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name().c_str(), - light_effect->get_first_universe(), light_effect->get_last_universe()); + ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(), + light_effect->get_last_universe()); light_effects_.insert(light_effect); @@ -95,8 +95,8 @@ void E131Component::remove_effect(E131AddressableLightEffect *light_effect) { return; } - ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name().c_str(), - light_effect->get_first_universe(), light_effect->get_last_universe()); + ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(), + light_effect->get_last_universe()); light_effects_.erase(light_effect); diff --git a/esphome/components/e131/e131_addressable_light_effect.cpp b/esphome/components/e131/e131_addressable_light_effect.cpp index 4d1f98ab6c..780e181f04 100644 --- a/esphome/components/e131/e131_addressable_light_effect.cpp +++ b/esphome/components/e131/e131_addressable_light_effect.cpp @@ -9,7 +9,7 @@ namespace e131 { static const char *const TAG = "e131_addressable_light_effect"; static const int MAX_DATA_SIZE = (sizeof(E131Packet::values) - 1); -E131AddressableLightEffect::E131AddressableLightEffect(const std::string &name) : AddressableLightEffect(name) {} +E131AddressableLightEffect::E131AddressableLightEffect(const char *name) : AddressableLightEffect(name) {} int E131AddressableLightEffect::get_data_per_universe() const { return get_lights_per_universe() * channels_; } @@ -58,8 +58,8 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + packet.count - 1)); auto *input_data = packet.values + 1; - ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %" PRId32 "-%d.", get_name().c_str(), universe, - output_offset, output_end); + ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %" PRId32 "-%d.", get_name(), universe, output_offset, + output_end); switch (channels_) { case E131_MONO: diff --git a/esphome/components/e131/e131_addressable_light_effect.h b/esphome/components/e131/e131_addressable_light_effect.h index 17d7bd2829..381e08163b 100644 --- a/esphome/components/e131/e131_addressable_light_effect.h +++ b/esphome/components/e131/e131_addressable_light_effect.h @@ -13,7 +13,7 @@ enum E131LightChannels { E131_MONO = 1, E131_RGB = 3, E131_RGBW = 4 }; class E131AddressableLightEffect : public light::AddressableLightEffect { public: - E131AddressableLightEffect(const std::string &name); + E131AddressableLightEffect(const char *name); void start() override; void stop() override; diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 9caccad634..9840112040 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -30,7 +30,7 @@ inline static uint8_t half_sin8(uint8_t v) { return sin16_c(uint16_t(v) * 128u) class AddressableLightEffect : public LightEffect { public: - explicit AddressableLightEffect(const std::string &name) : LightEffect(name) {} + explicit AddressableLightEffect(const char *name) : LightEffect(name) {} void start_internal() override { this->get_addressable_()->set_effect_active(true); this->get_addressable_()->clear_effect_data(); @@ -57,8 +57,7 @@ class AddressableLightEffect : public LightEffect { class AddressableLambdaLightEffect : public AddressableLightEffect { public: - AddressableLambdaLightEffect(const std::string &name, - std::function f, + AddressableLambdaLightEffect(const char *name, std::function f, uint32_t update_interval) : AddressableLightEffect(name), f_(std::move(f)), update_interval_(update_interval) {} void start() override { this->initial_run_ = true; } @@ -81,7 +80,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect { class AddressableRainbowLightEffect : public AddressableLightEffect { public: - explicit AddressableRainbowLightEffect(const std::string &name) : AddressableLightEffect(name) {} + explicit AddressableRainbowLightEffect(const char *name) : AddressableLightEffect(name) {} void apply(AddressableLight &it, const Color ¤t_color) override { ESPHSVColor hsv; hsv.value = 255; @@ -112,7 +111,7 @@ struct AddressableColorWipeEffectColor { class AddressableColorWipeEffect : public AddressableLightEffect { public: - explicit AddressableColorWipeEffect(const std::string &name) : AddressableLightEffect(name) {} + explicit AddressableColorWipeEffect(const char *name) : AddressableLightEffect(name) {} void set_colors(const std::initializer_list &colors) { this->colors_ = colors; } void set_add_led_interval(uint32_t add_led_interval) { this->add_led_interval_ = add_led_interval; } void set_reverse(bool reverse) { this->reverse_ = reverse; } @@ -165,7 +164,7 @@ class AddressableColorWipeEffect : public AddressableLightEffect { class AddressableScanEffect : public AddressableLightEffect { public: - explicit AddressableScanEffect(const std::string &name) : AddressableLightEffect(name) {} + explicit AddressableScanEffect(const char *name) : AddressableLightEffect(name) {} void set_move_interval(uint32_t move_interval) { this->move_interval_ = move_interval; } void set_scan_width(uint32_t scan_width) { this->scan_width_ = scan_width; } void apply(AddressableLight &it, const Color ¤t_color) override { @@ -202,7 +201,7 @@ class AddressableScanEffect : public AddressableLightEffect { class AddressableTwinkleEffect : public AddressableLightEffect { public: - explicit AddressableTwinkleEffect(const std::string &name) : AddressableLightEffect(name) {} + explicit AddressableTwinkleEffect(const char *name) : AddressableLightEffect(name) {} void apply(AddressableLight &addressable, const Color ¤t_color) override { const uint32_t now = millis(); uint8_t pos_add = 0; @@ -244,7 +243,7 @@ class AddressableTwinkleEffect : public AddressableLightEffect { class AddressableRandomTwinkleEffect : public AddressableLightEffect { public: - explicit AddressableRandomTwinkleEffect(const std::string &name) : AddressableLightEffect(name) {} + explicit AddressableRandomTwinkleEffect(const char *name) : AddressableLightEffect(name) {} void apply(AddressableLight &it, const Color ¤t_color) override { const uint32_t now = millis(); uint8_t pos_add = 0; @@ -293,7 +292,7 @@ class AddressableRandomTwinkleEffect : public AddressableLightEffect { class AddressableFireworksEffect : public AddressableLightEffect { public: - explicit AddressableFireworksEffect(const std::string &name) : AddressableLightEffect(name) {} + explicit AddressableFireworksEffect(const char *name) : AddressableLightEffect(name) {} void start() override { auto &it = *this->get_addressable_(); it.all() = Color::BLACK; @@ -342,7 +341,7 @@ class AddressableFireworksEffect : public AddressableLightEffect { class AddressableFlickerEffect : public AddressableLightEffect { public: - explicit AddressableFlickerEffect(const std::string &name) : AddressableLightEffect(name) {} + explicit AddressableFlickerEffect(const char *name) : AddressableLightEffect(name) {} void apply(AddressableLight &it, const Color ¤t_color) override { const uint32_t now = millis(); const uint8_t intensity = this->intensity_; diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index c74d19fe14..327c243525 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -17,7 +17,7 @@ inline static float random_cubic_float() { /// Pulse effect. class PulseLightEffect : public LightEffect { public: - explicit PulseLightEffect(const std::string &name) : LightEffect(name) {} + explicit PulseLightEffect(const char *name) : LightEffect(name) {} void apply() override { const uint32_t now = millis(); @@ -60,7 +60,7 @@ class PulseLightEffect : public LightEffect { /// Random effect. Sets random colors every 10 seconds and slowly transitions between them. class RandomLightEffect : public LightEffect { public: - explicit RandomLightEffect(const std::string &name) : LightEffect(name) {} + explicit RandomLightEffect(const char *name) : LightEffect(name) {} void apply() override { const uint32_t now = millis(); @@ -112,7 +112,7 @@ class RandomLightEffect : public LightEffect { class LambdaLightEffect : public LightEffect { public: - LambdaLightEffect(const std::string &name, std::function f, uint32_t update_interval) + LambdaLightEffect(const char *name, std::function f, uint32_t update_interval) : LightEffect(name), f_(std::move(f)), update_interval_(update_interval) {} void start() override { this->initial_run_ = true; } @@ -138,7 +138,7 @@ class LambdaLightEffect : public LightEffect { class AutomationLightEffect : public LightEffect { public: - AutomationLightEffect(const std::string &name) : LightEffect(name) {} + AutomationLightEffect(const char *name) : LightEffect(name) {} void stop() override { this->trig_->stop_action(); } void apply() override { if (!this->trig_->is_action_running()) { @@ -163,7 +163,7 @@ struct StrobeLightEffectColor { class StrobeLightEffect : public LightEffect { public: - explicit StrobeLightEffect(const std::string &name) : LightEffect(name) {} + explicit StrobeLightEffect(const char *name) : LightEffect(name) {} void apply() override { const uint32_t now = millis(); if (now - this->last_switch_ < this->colors_[this->at_color_].duration) @@ -198,7 +198,7 @@ class StrobeLightEffect : public LightEffect { class FlickerLightEffect : public LightEffect { public: - explicit FlickerLightEffect(const std::string &name) : LightEffect(name) {} + explicit FlickerLightEffect(const char *name) : LightEffect(name) {} void apply() override { LightColorValues remote = this->state_->remote_values; diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index af193e1f11..f611baba71 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -156,7 +156,7 @@ void LightCall::perform() { if (this->effect_ == 0u) { effect_s = "None"; } else { - effect_s = this->parent_->effects_[this->effect_ - 1]->get_name().c_str(); + effect_s = this->parent_->effects_[this->effect_ - 1]->get_name(); } if (publish) { @@ -511,7 +511,7 @@ LightCall &LightCall::set_effect(const std::string &effect) { for (uint32_t i = 0; i < this->parent_->effects_.size(); i++) { LightEffect *e = this->parent_->effects_[i]; - if (strcasecmp(effect.c_str(), e->get_name().c_str()) == 0) { + if (strcasecmp(effect.c_str(), e->get_name()) == 0) { this->set_effect(i + 1); found = true; break; diff --git a/esphome/components/light/light_effect.h b/esphome/components/light/light_effect.h index dbaf1faf24..d4c2dc3582 100644 --- a/esphome/components/light/light_effect.h +++ b/esphome/components/light/light_effect.h @@ -1,7 +1,5 @@ #pragma once -#include - #include "esphome/core/component.h" namespace esphome { @@ -11,7 +9,7 @@ class LightState; class LightEffect { public: - explicit LightEffect(std::string name) : name_(std::move(name)) {} + explicit LightEffect(const char *name) : name_(name) {} /// Initialize this LightEffect. Will be called once after creation. virtual void start() {} @@ -24,7 +22,11 @@ class LightEffect { /// Apply this effect. Use the provided state for starting transitions, ... virtual void apply() = 0; - const std::string &get_name() { return this->name_; } + /** + * Returns the name of this effect. + * The returned pointer is valid for the lifetime of the program and must not be freed. + */ + const char *get_name() const { return this->name_; } /// Internal method called by the LightState when this light effect is registered in it. virtual void init() {} @@ -47,7 +49,7 @@ class LightEffect { protected: LightState *state_{nullptr}; - std::string name_; + const char *name_; /// Internal method to find this effect's index in the parent light's effect list. uint32_t get_index_in_parent_() const; diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index a07aeb6ae5..502a08c635 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -177,7 +177,7 @@ class LightState : public EntityBase, public Component { return 0; } for (size_t i = 0; i < this->effects_.size(); i++) { - if (strcasecmp(effect_name.c_str(), this->effects_[i]->get_name().c_str()) == 0) { + if (strcasecmp(effect_name.c_str(), this->effects_[i]->get_name()) == 0) { return i + 1; // Effects are 1-indexed in active_effect_index_ } } diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp index 25577ccc11..d26b7a1750 100644 --- a/esphome/components/wled/wled_light_effect.cpp +++ b/esphome/components/wled/wled_light_effect.cpp @@ -28,7 +28,7 @@ const int DEFAULT_BLANK_TIME = 1000; static const char *const TAG = "wled_light_effect"; -WLEDLightEffect::WLEDLightEffect(const std::string &name) : AddressableLightEffect(name) {} +WLEDLightEffect::WLEDLightEffect(const char *name) : AddressableLightEffect(name) {} void WLEDLightEffect::start() { AddressableLightEffect::start(); diff --git a/esphome/components/wled/wled_light_effect.h b/esphome/components/wled/wled_light_effect.h index a591e1fd1a..6da5f4e9f9 100644 --- a/esphome/components/wled/wled_light_effect.h +++ b/esphome/components/wled/wled_light_effect.h @@ -15,7 +15,7 @@ namespace wled { class WLEDLightEffect : public light::AddressableLightEffect { public: - WLEDLightEffect(const std::string &name); + WLEDLightEffect(const char *name); void start() override; void stop() override; From 6efe346cc5bf18c29ff2a08042f51a6255dee9f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 15:21:53 -1000 Subject: [PATCH 034/394] [light] Use std::initializer_list for add_effects to reduce flash overhead (#11485) --- esphome/components/light/light_state.cpp | 7 ++----- esphome/components/light/light_state.h | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 979dc2f5a1..7b0a698bb8 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -178,12 +178,9 @@ void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore void LightState::set_initial_state(const LightStateRTCState &initial_state) { this->initial_state_ = initial_state; } bool LightState::supports_effects() { return !this->effects_.empty(); } const FixedVector &LightState::get_effects() const { return this->effects_; } -void LightState::add_effects(const std::vector &effects) { +void LightState::add_effects(const std::initializer_list &effects) { // Called once from Python codegen during setup with all effects from YAML config - this->effects_.init(effects.size()); - for (auto *effect : effects) { - this->effects_.push_back(effect); - } + this->effects_ = effects; } void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); } diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 502a08c635..bf63c0ec27 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -163,7 +163,7 @@ class LightState : public EntityBase, public Component { const FixedVector &get_effects() const; /// Add effects for this light state. - void add_effects(const std::vector &effects); + void add_effects(const std::initializer_list &effects); /// Get the total number of effects available for this light. size_t get_effect_count() const { return this->effects_.size(); } From 2864e989bdbf9948cf3822575346556624daff56 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 15:22:46 -1000 Subject: [PATCH 035/394] [light] Extract ColorModeMask into generic FiniteSetMask helper (#11472) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/light/color_mode.h | 215 +++++++----------------- esphome/components/light/light_call.cpp | 2 +- esphome/components/light/light_traits.h | 4 +- esphome/core/finite_set_mask.h | 171 +++++++++++++++++++ 4 files changed, 237 insertions(+), 155 deletions(-) create mode 100644 esphome/core/finite_set_mask.h diff --git a/esphome/components/light/color_mode.h b/esphome/components/light/color_mode.h index a26f917167..aa3448c145 100644 --- a/esphome/components/light/color_mode.h +++ b/esphome/components/light/color_mode.h @@ -1,6 +1,7 @@ #pragma once #include +#include "esphome/core/finite_set_mask.h" namespace esphome { namespace light { @@ -107,13 +108,9 @@ constexpr ColorModeHelper operator|(ColorModeHelper lhs, ColorMode rhs) { // Type alias for raw color mode bitmask values using color_mode_bitmask_t = uint16_t; -// Constants for ColorMode count and bit range -static constexpr int COLOR_MODE_COUNT = 10; // UNKNOWN through RGB_COLD_WARM_WHITE -static constexpr int MAX_BIT_INDEX = sizeof(color_mode_bitmask_t) * 8; // Number of bits in bitmask type - -// Compile-time array of all ColorMode values in declaration order -// Bit positions (0-9) map directly to enum declaration order -static constexpr ColorMode COLOR_MODES[COLOR_MODE_COUNT] = { +// Lookup table for ColorMode bit mapping +// This array defines the canonical order of color modes (bit 0-9) +constexpr ColorMode COLOR_MODE_LOOKUP[] = { ColorMode::UNKNOWN, // bit 0 ColorMode::ON_OFF, // bit 1 ColorMode::BRIGHTNESS, // bit 2 @@ -126,33 +123,42 @@ static constexpr ColorMode COLOR_MODES[COLOR_MODE_COUNT] = { ColorMode::RGB_COLD_WARM_WHITE, // bit 9 }; -/// Map ColorMode enum values to bit positions (0-9) -/// Bit positions follow the enum declaration order -static constexpr int mode_to_bit(ColorMode mode) { - // Linear search through COLOR_MODES array - // Compiler optimizes this to efficient code since array is constexpr - for (int i = 0; i < COLOR_MODE_COUNT; ++i) { - if (COLOR_MODES[i] == mode) - return i; - } - return 0; -} +/// Bit mapping policy for ColorMode +/// Uses lookup table for non-contiguous enum values +struct ColorModeBitPolicy { + using mask_t = uint16_t; // 10 bits requires uint16_t + static constexpr int MAX_BITS = sizeof(COLOR_MODE_LOOKUP) / sizeof(COLOR_MODE_LOOKUP[0]); -/// Map bit positions (0-9) to ColorMode enum values -/// Bit positions follow the enum declaration order -static constexpr ColorMode bit_to_mode(int bit) { - // Direct lookup in COLOR_MODES array - return (bit >= 0 && bit < COLOR_MODE_COUNT) ? COLOR_MODES[bit] : ColorMode::UNKNOWN; -} + static constexpr unsigned to_bit(ColorMode mode) { + // Linear search through lookup table + // Compiler optimizes this to efficient code since array is constexpr + for (int i = 0; i < MAX_BITS; ++i) { + if (COLOR_MODE_LOOKUP[i] == mode) + return i; + } + return 0; + } + + static constexpr ColorMode from_bit(unsigned bit) { + return (bit < MAX_BITS) ? COLOR_MODE_LOOKUP[bit] : ColorMode::UNKNOWN; + } +}; + +// Type alias for ColorMode bitmask using policy-based design +using ColorModeMask = FiniteSetMask; + +// Number of ColorCapability enum values +constexpr int COLOR_CAPABILITY_COUNT = 6; /// Helper to compute capability bitmask at compile time -static constexpr color_mode_bitmask_t compute_capability_bitmask(ColorCapability capability) { - color_mode_bitmask_t mask = 0; +constexpr uint16_t compute_capability_bitmask(ColorCapability capability) { + uint16_t mask = 0; uint8_t cap_bit = static_cast(capability); // Check each ColorMode to see if it has this capability - for (int bit = 0; bit < COLOR_MODE_COUNT; ++bit) { - uint8_t mode_val = static_cast(bit_to_mode(bit)); + constexpr int color_mode_count = sizeof(COLOR_MODE_LOOKUP) / sizeof(COLOR_MODE_LOOKUP[0]); + for (int bit = 0; bit < color_mode_count; ++bit) { + uint8_t mode_val = static_cast(COLOR_MODE_LOOKUP[bit]); if ((mode_val & cap_bit) != 0) { mask |= (1 << bit); } @@ -160,12 +166,9 @@ static constexpr color_mode_bitmask_t compute_capability_bitmask(ColorCapability return mask; } -// Number of ColorCapability enum values -static constexpr int COLOR_CAPABILITY_COUNT = 6; - /// Compile-time lookup table mapping ColorCapability to bitmask /// This array is computed at compile time using constexpr -static constexpr color_mode_bitmask_t CAPABILITY_BITMASKS[] = { +constexpr uint16_t CAPABILITY_BITMASKS[] = { compute_capability_bitmask(ColorCapability::ON_OFF), // 1 << 0 compute_capability_bitmask(ColorCapability::BRIGHTNESS), // 1 << 1 compute_capability_bitmask(ColorCapability::WHITE), // 1 << 2 @@ -174,130 +177,38 @@ static constexpr color_mode_bitmask_t CAPABILITY_BITMASKS[] = { compute_capability_bitmask(ColorCapability::RGB), // 1 << 5 }; -/// Bitmask for storing a set of ColorMode values efficiently. -/// Replaces std::set to eliminate red-black tree overhead (~586 bytes). -class ColorModeMask { - public: - constexpr ColorModeMask() = default; - - /// Support initializer list syntax: {ColorMode::RGB, ColorMode::WHITE} - constexpr ColorModeMask(std::initializer_list modes) { - for (auto mode : modes) { - this->add(mode); - } - } - - constexpr void add(ColorMode mode) { this->mask_ |= (1 << mode_to_bit(mode)); } - - /// Add multiple modes at once using initializer list - constexpr void add(std::initializer_list modes) { - for (auto mode : modes) { - this->add(mode); - } - } - - constexpr bool contains(ColorMode mode) const { return (this->mask_ & (1 << mode_to_bit(mode))) != 0; } - - constexpr size_t size() const { - // Count set bits using Brian Kernighan's algorithm - // More efficient for sparse bitmasks (typical case: 2-4 modes out of 10) - uint16_t n = this->mask_; - size_t count = 0; - while (n) { - n &= n - 1; // Clear the least significant set bit - count++; - } - return count; - } - - constexpr bool empty() const { return this->mask_ == 0; } - - /// Iterator support for API encoding - class Iterator { - public: - using iterator_category = std::forward_iterator_tag; - using value_type = ColorMode; - using difference_type = std::ptrdiff_t; - using pointer = const ColorMode *; - using reference = ColorMode; - - constexpr Iterator(color_mode_bitmask_t mask, int bit) : mask_(mask), bit_(bit) { advance_to_next_set_bit_(); } - - constexpr ColorMode operator*() const { return bit_to_mode(bit_); } - - constexpr Iterator &operator++() { - ++bit_; - advance_to_next_set_bit_(); - return *this; - } - - constexpr bool operator==(const Iterator &other) const { return bit_ == other.bit_; } - - constexpr bool operator!=(const Iterator &other) const { return !(*this == other); } - - private: - constexpr void advance_to_next_set_bit_() { bit_ = ColorModeMask::find_next_set_bit(mask_, bit_); } - - color_mode_bitmask_t mask_; - int bit_; - }; - - constexpr Iterator begin() const { return Iterator(mask_, 0); } - constexpr Iterator end() const { return Iterator(mask_, MAX_BIT_INDEX); } - - /// Get the raw bitmask value for API encoding - constexpr color_mode_bitmask_t get_mask() const { return this->mask_; } - - /// Find the next set bit in a bitmask starting from a given position - /// Returns the bit position, or MAX_BIT_INDEX if no more bits are set - static constexpr int find_next_set_bit(color_mode_bitmask_t mask, int start_bit) { - int bit = start_bit; - while (bit < MAX_BIT_INDEX && !(mask & (1 << bit))) { - ++bit; - } - return bit; - } - - /// Find the first set bit in a bitmask and return the corresponding ColorMode - /// Used for optimizing compute_color_mode_() intersection logic - static constexpr ColorMode first_mode_from_mask(color_mode_bitmask_t mask) { - return bit_to_mode(find_next_set_bit(mask, 0)); - } - - /// Check if a ColorMode is present in a raw bitmask value - /// Useful for checking intersection results without creating a temporary ColorModeMask - static constexpr bool mask_contains(color_mode_bitmask_t mask, ColorMode mode) { - return (mask & (1 << mode_to_bit(mode))) != 0; - } - - /// Check if any mode in the bitmask has a specific capability - /// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB) - bool has_capability(ColorCapability capability) const { - // Lookup the pre-computed bitmask for this capability and check intersection with our mask - // ColorCapability values: 1, 2, 4, 8, 16, 32 -> array indices: 0, 1, 2, 3, 4, 5 - // We need to convert the power-of-2 value to an index - uint8_t cap_val = static_cast(capability); +/** + * @brief Helper function to convert a power-of-2 ColorCapability value to an array index for CAPABILITY_BITMASKS + * lookup. + * + * This function maps ColorCapability values (1, 2, 4, 8, 16, 32) to array indices (0, 1, 2, 3, 4, 5). + * Used to index into the CAPABILITY_BITMASKS lookup table. + * + * @param capability A ColorCapability enum value (must be a power of 2). + * @return The corresponding array index (0-based). + */ +inline int capability_to_index(ColorCapability capability) { + uint8_t cap_val = static_cast(capability); #if defined(__GNUC__) || defined(__clang__) - // Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n)) - int index = __builtin_ctz(cap_val); + // Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n)) + return __builtin_ctz(cap_val); #else - // Fallback for compilers without __builtin_ctz - int index = 0; - while (cap_val > 1) { - cap_val >>= 1; - ++index; - } -#endif - return (this->mask_ & CAPABILITY_BITMASKS[index]) != 0; + // Fallback for compilers without __builtin_ctz + int index = 0; + while (cap_val > 1) { + cap_val >>= 1; + ++index; } + return index; +#endif +} - private: - // Using uint16_t instead of uint32_t for more efficient iteration (fewer bits to scan). - // Currently only 10 ColorMode values exist, so 16 bits is sufficient. - // Can be changed to uint32_t if more than 16 color modes are needed in the future. - // Note: Due to struct padding, uint16_t and uint32_t result in same LightTraits size (12 bytes). - color_mode_bitmask_t mask_{0}; -}; +/// Check if any mode in the bitmask has a specific capability +/// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB) +inline bool has_capability(const ColorModeMask &mask, ColorCapability capability) { + // Lookup the pre-computed bitmask for this capability and check intersection with our mask + return (mask.get_mask() & CAPABILITY_BITMASKS[capability_to_index(capability)]) != 0; +} } // namespace light } // namespace esphome diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index f611baba71..df17f53adc 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -437,7 +437,7 @@ ColorMode LightCall::compute_color_mode_() { // Use the preferred suitable mode. if (intersection != 0) { - ColorMode mode = ColorModeMask::first_mode_from_mask(intersection); + ColorMode mode = ColorModeMask::first_value_from_mask(intersection); ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(mode))); return mode; diff --git a/esphome/components/light/light_traits.h b/esphome/components/light/light_traits.h index 4532edca83..294b0cad1d 100644 --- a/esphome/components/light/light_traits.h +++ b/esphome/components/light/light_traits.h @@ -26,9 +26,9 @@ class LightTraits { this->supported_color_modes_ = ColorModeMask(modes); } - bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.contains(color_mode); } + bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.count(color_mode) > 0; } bool supports_color_capability(ColorCapability color_capability) const { - return this->supported_color_modes_.has_capability(color_capability); + return has_capability(this->supported_color_modes_, color_capability); } float get_min_mireds() const { return this->min_mireds_; } diff --git a/esphome/core/finite_set_mask.h b/esphome/core/finite_set_mask.h new file mode 100644 index 0000000000..f9cd0377c7 --- /dev/null +++ b/esphome/core/finite_set_mask.h @@ -0,0 +1,171 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace esphome { + +/// Default bit mapping policy for contiguous enums starting at 0 +/// Provides 1:1 mapping where enum value equals bit position +template struct DefaultBitPolicy { + // Automatic bitmask type selection based on MaxBits + // ≤8 bits: uint8_t, ≤16 bits: uint16_t, otherwise: uint32_t + using mask_t = typename std::conditional<(MaxBits <= 8), uint8_t, + typename std::conditional<(MaxBits <= 16), uint16_t, uint32_t>::type>::type; + + static constexpr int MAX_BITS = MaxBits; + + static constexpr unsigned to_bit(ValueType value) { return static_cast(value); } + + static constexpr ValueType from_bit(unsigned bit) { return static_cast(bit); } +}; + +/// Generic bitmask for storing a finite set of discrete values efficiently. +/// Replaces std::set to eliminate red-black tree overhead (~586 bytes per instantiation). +/// +/// Template parameters: +/// ValueType: The type to store (typically enum, but can be any discrete bounded type) +/// BitPolicy: Policy class defining bit mapping and mask type (defaults to DefaultBitPolicy) +/// +/// BitPolicy requirements: +/// - using mask_t = // Bitmask storage type +/// - static constexpr int MAX_BITS // Maximum number of bits +/// - static constexpr unsigned to_bit(ValueType) // Convert value to bit position +/// - static constexpr ValueType from_bit(unsigned) // Convert bit position to value +/// +/// Example usage (1:1 mapping - climate enums): +/// // For contiguous enums starting at 0, use DefaultBitPolicy +/// using ClimateModeMask = FiniteSetMask>; +/// ClimateModeMask modes({CLIMATE_MODE_HEAT, CLIMATE_MODE_COOL}); +/// if (modes.count(CLIMATE_MODE_HEAT)) { ... } +/// for (auto mode : modes) { ... } +/// +/// Example usage (custom mapping - ColorMode): +/// // For custom mappings, define a custom BitPolicy +/// // See esphome/components/light/color_mode.h for complete example +/// +/// Design notes: +/// - Policy-based design allows custom bit mappings without template specialization +/// - Iterator converts bit positions to actual values during traversal +/// - All operations are constexpr-compatible for compile-time initialization +/// - Drop-in replacement for std::set with simpler API +/// +template> class FiniteSetMask { + public: + using bitmask_t = typename BitPolicy::mask_t; + + constexpr FiniteSetMask() = default; + + /// Construct from initializer list: {VALUE1, VALUE2, ...} + constexpr FiniteSetMask(std::initializer_list values) { + for (auto value : values) { + this->insert(value); + } + } + + /// Add a single value to the set (std::set compatibility) + constexpr void insert(ValueType value) { this->mask_ |= (static_cast(1) << BitPolicy::to_bit(value)); } + + /// Add multiple values from initializer list + constexpr void insert(std::initializer_list values) { + for (auto value : values) { + this->insert(value); + } + } + + /// Remove a value from the set (std::set compatibility) + constexpr void erase(ValueType value) { this->mask_ &= ~(static_cast(1) << BitPolicy::to_bit(value)); } + + /// Clear all values from the set + constexpr void clear() { this->mask_ = 0; } + + /// Check if the set contains a specific value (std::set compatibility) + /// Returns 1 if present, 0 if not (same as std::set for unique elements) + constexpr size_t count(ValueType value) const { + return (this->mask_ & (static_cast(1) << BitPolicy::to_bit(value))) != 0 ? 1 : 0; + } + + /// Count the number of values in the set + constexpr size_t size() const { + // Brian Kernighan's algorithm - efficient for sparse bitmasks + // Typical case: 2-4 modes out of 10 possible + bitmask_t n = this->mask_; + size_t count = 0; + while (n) { + n &= n - 1; // Clear the least significant set bit + count++; + } + return count; + } + + /// Check if the set is empty + constexpr bool empty() const { return this->mask_ == 0; } + + /// Iterator support for range-based for loops and API encoding + /// Iterates over set bits and converts bit positions to values + /// Optimization: removes bits from mask as we iterate + class Iterator { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = ValueType; + using difference_type = std::ptrdiff_t; + using pointer = const ValueType *; + using reference = ValueType; + + constexpr explicit Iterator(bitmask_t mask) : mask_(mask) {} + + constexpr ValueType operator*() const { + // Return value for the first set bit + return BitPolicy::from_bit(find_next_set_bit(mask_, 0)); + } + + constexpr Iterator &operator++() { + // Clear the lowest set bit (Brian Kernighan's algorithm) + mask_ &= mask_ - 1; + return *this; + } + + constexpr bool operator==(const Iterator &other) const { return mask_ == other.mask_; } + + constexpr bool operator!=(const Iterator &other) const { return !(*this == other); } + + private: + bitmask_t mask_; + }; + + constexpr Iterator begin() const { return Iterator(mask_); } + constexpr Iterator end() const { return Iterator(0); } + + /// Get the raw bitmask value for optimized operations + constexpr bitmask_t get_mask() const { return this->mask_; } + + /// Check if a specific value is present in a raw bitmask + /// Useful for checking intersection results without creating temporary objects + static constexpr bool mask_contains(bitmask_t mask, ValueType value) { + return (mask & (static_cast(1) << BitPolicy::to_bit(value))) != 0; + } + + /// Get the first value from a raw bitmask + /// Used for optimizing intersection logic (e.g., "pick first suitable mode") + static constexpr ValueType first_value_from_mask(bitmask_t mask) { + return BitPolicy::from_bit(find_next_set_bit(mask, 0)); + } + + /// Find the next set bit in a bitmask starting from a given position + /// Returns the bit position, or MAX_BITS if no more bits are set + static constexpr int find_next_set_bit(bitmask_t mask, int start_bit) { + int bit = start_bit; + while (bit < BitPolicy::MAX_BITS && !(mask & (static_cast(1) << bit))) { + ++bit; + } + return bit; + } + + protected: + bitmask_t mask_{0}; +}; + +} // namespace esphome From 7e5b82c5f3ece45a0f7c9d98eeecc1c2cf93126d Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 22 Oct 2025 20:24:08 -0500 Subject: [PATCH 036/394] [improv_serial] Various optimizations (#11473) Co-authored-by: J. Nick Koston --- .../improv_serial/improv_serial_component.cpp | 183 +++++++++--------- .../improv_serial/improv_serial_component.h | 27 ++- .../components/improv_base/common-uart0.yaml | 8 + .../improv_base/test-uart0.esp8266-ard.yaml | 1 + 4 files changed, 128 insertions(+), 91 deletions(-) create mode 100644 tests/components/improv_base/common-uart0.yaml create mode 100644 tests/components/improv_base/test-uart0.esp8266-ard.yaml diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index ce82504d3c..9d080ea98e 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -28,6 +28,38 @@ void ImprovSerialComponent::setup() { } } +void ImprovSerialComponent::loop() { + if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) { + this->last_read_byte_ = 0; + this->rx_buffer_.clear(); + ESP_LOGV(TAG, "Timeout"); + } + + auto byte = this->read_byte_(); + while (byte.has_value()) { + if (this->parse_improv_serial_byte_(byte.value())) { + this->last_read_byte_ = millis(); + } else { + this->last_read_byte_ = 0; + this->rx_buffer_.clear(); + } + byte = this->read_byte_(); + } + + if (this->state_ == improv::STATE_PROVISIONING) { + if (wifi::global_wifi_component->is_connected()) { + wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), + this->connecting_sta_.get_password()); + this->connecting_sta_ = {}; + this->cancel_timeout("wifi-connect-timeout"); + this->set_state_(improv::STATE_PROVISIONED); + + std::vector url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS); + this->send_response_(url); + } + } +} + void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); } optional ImprovSerialComponent::read_byte_() { @@ -78,8 +110,28 @@ optional ImprovSerialComponent::read_byte_() { return byte; } -void ImprovSerialComponent::write_data_(std::vector &data) { - data.push_back('\n'); +void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size) { + // First, set length field + this->tx_header_[TX_LENGTH_IDX] = this->tx_header_[TX_TYPE_IDX] == TYPE_RPC_RESPONSE ? size : 1; + + const bool there_is_data = data != nullptr && size > 0; + // If there_is_data, checksum must not include our optional data byte + const uint8_t header_checksum_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE - 2; + // Only transmit the full buffer length if there is no data (only state/error byte is provided in this case) + const uint8_t header_tx_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE; + // Calculate checksum for message + uint8_t checksum = 0; + for (uint8_t i = 0; i < header_checksum_len; i++) { + checksum += this->tx_header_[i]; + } + if (there_is_data) { + // Include data in checksum + for (size_t i = 0; i < size; i++) { + checksum += data[i]; + } + } + this->tx_header_[TX_CHECKSUM_IDX] = checksum; + #ifdef USE_ESP32 switch (logger::global_logger->get_uart()) { case logger::UART_SELECTION_UART0: @@ -87,63 +139,45 @@ void ImprovSerialComponent::write_data_(std::vector &data) { #if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \ !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) case logger::UART_SELECTION_UART2: -#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3 - uart_write_bytes(this->uart_num_, data.data(), data.size()); +#endif + uart_write_bytes(this->uart_num_, this->tx_header_, header_tx_len); + if (there_is_data) { + uart_write_bytes(this->uart_num_, data, size); + uart_write_bytes(this->uart_num_, &this->tx_header_[TX_CHECKSUM_IDX], 2); // Footer: checksum and newline + } break; #if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC) - case logger::UART_SELECTION_USB_CDC: { - const char *msg = (char *) data.data(); - esp_usb_console_write_buf(msg, data.size()); + case logger::UART_SELECTION_USB_CDC: + esp_usb_console_write_buf((const char *) this->tx_header_, header_tx_len); + if (there_is_data) { + esp_usb_console_write_buf((const char *) data, size); + esp_usb_console_write_buf((const char *) &this->tx_header_[TX_CHECKSUM_IDX], + 2); // Footer: checksum and newline + } break; - } -#endif // USE_LOGGER_USB_CDC +#endif #ifdef USE_LOGGER_USB_SERIAL_JTAG case logger::UART_SELECTION_USB_SERIAL_JTAG: - usb_serial_jtag_write_bytes((char *) data.data(), data.size(), 20 / portTICK_PERIOD_MS); - delay(10); - usb_serial_jtag_ll_txfifo_flush(); // fixes for issue in IDF 4.4.7 + usb_serial_jtag_write_bytes((const char *) this->tx_header_, header_tx_len, 20 / portTICK_PERIOD_MS); + if (there_is_data) { + usb_serial_jtag_write_bytes((const char *) data, size, 20 / portTICK_PERIOD_MS); + usb_serial_jtag_write_bytes((const char *) &this->tx_header_[TX_CHECKSUM_IDX], 2, + 20 / portTICK_PERIOD_MS); // Footer: checksum and newline + } break; -#endif // USE_LOGGER_USB_SERIAL_JTAG +#endif default: break; } #elif defined(USE_ARDUINO) - this->hw_serial_->write(data.data(), data.size()); + this->hw_serial_->write(this->tx_header_, header_tx_len); + if (there_is_data) { + this->hw_serial_->write(data, size); + this->hw_serial_->write(&this->tx_header_[TX_CHECKSUM_IDX], 2); // Footer: checksum and newline + } #endif } -void ImprovSerialComponent::loop() { - if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) { - this->last_read_byte_ = 0; - this->rx_buffer_.clear(); - ESP_LOGV(TAG, "Improv Serial timeout"); - } - - auto byte = this->read_byte_(); - while (byte.has_value()) { - if (this->parse_improv_serial_byte_(byte.value())) { - this->last_read_byte_ = millis(); - } else { - this->last_read_byte_ = 0; - this->rx_buffer_.clear(); - } - byte = this->read_byte_(); - } - - if (this->state_ == improv::STATE_PROVISIONING) { - if (wifi::global_wifi_component->is_connected()) { - wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), - this->connecting_sta_.get_password()); - this->connecting_sta_ = {}; - this->cancel_timeout("wifi-connect-timeout"); - this->set_state_(improv::STATE_PROVISIONED); - - std::vector url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS); - this->send_response_(url); - } - } -} - std::vector ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) { std::vector urls; #ifdef USE_IMPROV_SERIAL_NEXT_URL @@ -177,13 +211,13 @@ std::vector ImprovSerialComponent::build_version_info_() { bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) { size_t at = this->rx_buffer_.size(); this->rx_buffer_.push_back(byte); - ESP_LOGV(TAG, "Improv Serial byte: 0x%02X", byte); + ESP_LOGV(TAG, "Byte: 0x%02X", byte); const uint8_t *raw = &this->rx_buffer_[0]; return improv::parse_improv_serial_byte( at, byte, raw, [this](improv::ImprovCommand command) -> bool { return this->parse_improv_payload_(command); }, [this](improv::Error error) -> void { - ESP_LOGW(TAG, "Error decoding Improv payload"); + ESP_LOGW(TAG, "Error decoding payload"); this->set_error_(error); }); } @@ -199,7 +233,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command wifi::global_wifi_component->set_sta(sta); wifi::global_wifi_component->start_connecting(sta, false); this->set_state_(improv::STATE_PROVISIONING); - ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), + ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); auto f = std::bind(&ImprovSerialComponent::on_wifi_connect_timeout_, this); @@ -240,7 +274,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command return true; } default: { - ESP_LOGW(TAG, "Unknown Improv payload"); + ESP_LOGW(TAG, "Unknown payload"); this->set_error_(improv::ERROR_UNKNOWN_RPC); return false; } @@ -249,57 +283,26 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command void ImprovSerialComponent::set_state_(improv::State state) { this->state_ = state; - - std::vector data = {'I', 'M', 'P', 'R', 'O', 'V'}; - data.resize(11); - data[6] = IMPROV_SERIAL_VERSION; - data[7] = TYPE_CURRENT_STATE; - data[8] = 1; - data[9] = state; - - uint8_t checksum = 0x00; - for (uint8_t d : data) - checksum += d; - data[10] = checksum; - - this->write_data_(data); + this->tx_header_[TX_TYPE_IDX] = TYPE_CURRENT_STATE; + this->tx_header_[TX_DATA_IDX] = state; + this->write_data_(); } void ImprovSerialComponent::set_error_(improv::Error error) { - std::vector data = {'I', 'M', 'P', 'R', 'O', 'V'}; - data.resize(11); - data[6] = IMPROV_SERIAL_VERSION; - data[7] = TYPE_ERROR_STATE; - data[8] = 1; - data[9] = error; - - uint8_t checksum = 0x00; - for (uint8_t d : data) - checksum += d; - data[10] = checksum; - this->write_data_(data); + this->tx_header_[TX_TYPE_IDX] = TYPE_ERROR_STATE; + this->tx_header_[TX_DATA_IDX] = error; + this->write_data_(); } void ImprovSerialComponent::send_response_(std::vector &response) { - std::vector data = {'I', 'M', 'P', 'R', 'O', 'V'}; - data.resize(9); - data[6] = IMPROV_SERIAL_VERSION; - data[7] = TYPE_RPC_RESPONSE; - data[8] = response.size(); - data.insert(data.end(), response.begin(), response.end()); - - uint8_t checksum = 0x00; - for (uint8_t d : data) - checksum += d; - data.push_back(checksum); - - this->write_data_(data); + this->tx_header_[TX_TYPE_IDX] = TYPE_RPC_RESPONSE; + this->write_data_(response.data(), response.size()); } void ImprovSerialComponent::on_wifi_connect_timeout_() { this->set_error_(improv::ERROR_UNABLE_TO_CONNECT); this->set_state_(improv::STATE_AUTHORIZED); - ESP_LOGW(TAG, "Timed out trying to connect to given WiFi network"); + ESP_LOGW(TAG, "Timed out while connecting to Wi-Fi network"); wifi::global_wifi_component->clear_sta(); } diff --git a/esphome/components/improv_serial/improv_serial_component.h b/esphome/components/improv_serial/improv_serial_component.h index c3c9aee24e..057247f376 100644 --- a/esphome/components/improv_serial/improv_serial_component.h +++ b/esphome/components/improv_serial/improv_serial_component.h @@ -26,6 +26,16 @@ namespace esphome { namespace improv_serial { +// TX buffer layout constants +static constexpr uint8_t TX_HEADER_SIZE = 6; // Bytes 0-5 = "IMPROV" +static constexpr uint8_t TX_VERSION_IDX = 6; +static constexpr uint8_t TX_TYPE_IDX = 7; +static constexpr uint8_t TX_LENGTH_IDX = 8; +static constexpr uint8_t TX_DATA_IDX = 9; // For state/error messages only +static constexpr uint8_t TX_CHECKSUM_IDX = 10; +static constexpr uint8_t TX_NEWLINE_IDX = 11; +static constexpr uint8_t TX_BUFFER_SIZE = 12; + enum ImprovSerialType : uint8_t { TYPE_CURRENT_STATE = 0x01, TYPE_ERROR_STATE = 0x02, @@ -57,7 +67,22 @@ class ImprovSerialComponent : public Component, public improv_base::ImprovBase { std::vector build_version_info_(); optional read_byte_(); - void write_data_(std::vector &data); + void write_data_(const uint8_t *data = nullptr, size_t size = 0); + + uint8_t tx_header_[TX_BUFFER_SIZE] = { + 'I', // 0: Header + 'M', // 1: Header + 'P', // 2: Header + 'R', // 3: Header + 'O', // 4: Header + 'V', // 5: Header + IMPROV_SERIAL_VERSION, // 6: Version + 0, // 7: ImprovSerialType + 0, // 8: Length + 0, // 9...X: Data (here, one byte reserved for state/error) + 0, // X + 10: Checksum + '\n', + }; #ifdef USE_ESP32 uart_port_t uart_num_; diff --git a/tests/components/improv_base/common-uart0.yaml b/tests/components/improv_base/common-uart0.yaml new file mode 100644 index 0000000000..7b7730fd46 --- /dev/null +++ b/tests/components/improv_base/common-uart0.yaml @@ -0,0 +1,8 @@ +wifi: + ssid: MySSID + password: password1 + +logger: + hardware_uart: UART0 + +improv_serial: diff --git a/tests/components/improv_base/test-uart0.esp8266-ard.yaml b/tests/components/improv_base/test-uart0.esp8266-ard.yaml new file mode 100644 index 0000000000..ef8c799241 --- /dev/null +++ b/tests/components/improv_base/test-uart0.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common-uart0.yaml From 6338326d10d0830d2a177aad7681f4b0f2cc9ac6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 16:18:57 -1000 Subject: [PATCH 037/394] use helper to fix flakey test --- tests/integration/state_utils.py | 6 ++++ .../test_host_mode_climate_basic_state.py | 34 +++++++++---------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/tests/integration/state_utils.py b/tests/integration/state_utils.py index 58d6d2790f..6434a41ddf 100644 --- a/tests/integration/state_utils.py +++ b/tests/integration/state_utils.py @@ -44,6 +44,7 @@ class InitialStateHelper: helper = InitialStateHelper(entities) client.subscribe_states(helper.on_state_wrapper(user_callback)) await helper.wait_for_initial_states() + # Access initial states via helper.initial_states[key] """ def __init__(self, entities: list[EntityInfo]) -> None: @@ -63,6 +64,8 @@ class InitialStateHelper: self._entities_by_id = { (entity.device_id, entity.key): entity for entity in entities } + # Store initial states by key for test access + self.initial_states: dict[int, EntityState] = {} # Log all entities _LOGGER.debug( @@ -127,6 +130,9 @@ class InitialStateHelper: # If this entity is waiting for initial state if entity_id in self._wait_initial_states: + # Store the initial state for test access + self.initial_states[state.key] = state + # Remove from waiting set self._wait_initial_states.discard(entity_id) diff --git a/tests/integration/test_host_mode_climate_basic_state.py b/tests/integration/test_host_mode_climate_basic_state.py index 4697342a99..7d871ed5a8 100644 --- a/tests/integration/test_host_mode_climate_basic_state.py +++ b/tests/integration/test_host_mode_climate_basic_state.py @@ -2,12 +2,11 @@ from __future__ import annotations -import asyncio - import aioesphomeapi -from aioesphomeapi import ClimateAction, ClimateMode, ClimatePreset, EntityState +from aioesphomeapi import ClimateAction, ClimateInfo, ClimateMode, ClimatePreset import pytest +from .state_utils import InitialStateHelper from .types import APIClientConnectedFactory, RunCompiledFunction @@ -18,26 +17,27 @@ async def test_host_mode_climate_basic_state( api_client_connected: APIClientConnectedFactory, ) -> None: """Test basic climate state reporting.""" - loop = asyncio.get_running_loop() async with run_compiled(yaml_config), api_client_connected() as client: - states: dict[int, EntityState] = {} - climate_future: asyncio.Future[EntityState] = loop.create_future() + # Get entities and set up state synchronization + entities, services = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] + assert len(climate_infos) >= 1, "Expected at least 1 climate entity" - def on_state(state: EntityState) -> None: - states[state.key] = state - if ( - isinstance(state, aioesphomeapi.ClimateState) - and not climate_future.done() - ): - climate_future.set_result(state) - - client.subscribe_states(on_state) + # Subscribe with the wrapper (no-op callback since we just want initial states) + client.subscribe_states(initial_state_helper.on_state_wrapper(lambda _: None)) + # Wait for all initial states to be broadcast try: - climate_state = await asyncio.wait_for(climate_future, timeout=5.0) + await initial_state_helper.wait_for_initial_states() except TimeoutError: - pytest.fail("Climate state not received within 5 seconds") + pytest.fail("Timeout waiting for initial states") + # Get the climate entity and its initial state + test_climate = climate_infos[0] + climate_state = initial_state_helper.initial_states.get(test_climate.key) + + assert climate_state is not None, "Climate initial state not found" assert isinstance(climate_state, aioesphomeapi.ClimateState) assert climate_state.mode == ClimateMode.OFF assert climate_state.action == ClimateAction.OFF From 9b78098eec3c965df24933d2230885381997b399 Mon Sep 17 00:00:00 2001 From: optimusprimespace <62800678+optimusprimespace@users.noreply.github.com> Date: Thu, 23 Oct 2025 05:24:17 +0300 Subject: [PATCH 038/394] [hdc2010] New component (#6674) Co-authored-by: Keith Burzinski Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/hdc2010/__init__.py | 1 + esphome/components/hdc2010/hdc2010.cpp | 111 ++++++++++++++++++ esphome/components/hdc2010/hdc2010.h | 32 +++++ esphome/components/hdc2010/sensor.py | 56 +++++++++ tests/components/hdc2010/common.yaml | 7 ++ .../components/hdc2010/test.esp32-c3-idf.yaml | 4 + tests/components/hdc2010/test.esp32-idf.yaml | 4 + .../components/hdc2010/test.esp8266-ard.yaml | 4 + tests/components/hdc2010/test.rp2040-ard.yaml | 4 + 10 files changed, 224 insertions(+) create mode 100644 esphome/components/hdc2010/__init__.py create mode 100644 esphome/components/hdc2010/hdc2010.cpp create mode 100644 esphome/components/hdc2010/hdc2010.h create mode 100644 esphome/components/hdc2010/sensor.py create mode 100644 tests/components/hdc2010/common.yaml create mode 100644 tests/components/hdc2010/test.esp32-c3-idf.yaml create mode 100644 tests/components/hdc2010/test.esp32-idf.yaml create mode 100644 tests/components/hdc2010/test.esp8266-ard.yaml create mode 100644 tests/components/hdc2010/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 4f860375d9..667a44fc03 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -201,6 +201,7 @@ esphome/components/havells_solar/* @sourabhjaiswal esphome/components/hbridge/fan/* @WeekendWarrior esphome/components/hbridge/light/* @DotNetDann esphome/components/hbridge/switch/* @dwmw2 +esphome/components/hdc2010/* @optimusprimespace @ssieb esphome/components/he60r/* @clydebarrow esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal diff --git a/esphome/components/hdc2010/__init__.py b/esphome/components/hdc2010/__init__.py new file mode 100644 index 0000000000..badf9dbb0c --- /dev/null +++ b/esphome/components/hdc2010/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@optimusprimespace", "@ssieb"] diff --git a/esphome/components/hdc2010/hdc2010.cpp b/esphome/components/hdc2010/hdc2010.cpp new file mode 100644 index 0000000000..c53fdb3f5b --- /dev/null +++ b/esphome/components/hdc2010/hdc2010.cpp @@ -0,0 +1,111 @@ +#include "esphome/core/hal.h" +#include "hdc2010.h" +// https://github.com/vigsterkr/homebridge-hdc2010/blob/main/src/hdc2010.js +// https://github.com/lime-labs/HDC2080-Arduino/blob/master/src/HDC2080.cpp +namespace esphome { +namespace hdc2010 { + +static const char *const TAG = "hdc2010"; + +static const uint8_t HDC2010_ADDRESS = 0x40; // 0b1000000 or 0b1000001 from datasheet +static const uint8_t HDC2010_CMD_CONFIGURATION_MEASUREMENT = 0x8F; +static const uint8_t HDC2010_CMD_START_MEASUREMENT = 0xF9; +static const uint8_t HDC2010_CMD_TEMPERATURE_LOW = 0x00; +static const uint8_t HDC2010_CMD_TEMPERATURE_HIGH = 0x01; +static const uint8_t HDC2010_CMD_HUMIDITY_LOW = 0x02; +static const uint8_t HDC2010_CMD_HUMIDITY_HIGH = 0x03; +static const uint8_t CONFIG = 0x0E; +static const uint8_t MEASUREMENT_CONFIG = 0x0F; + +void HDC2010Component::setup() { + ESP_LOGCONFIG(TAG, "Running setup"); + + const uint8_t data[2] = { + 0b00000000, // resolution 14bit for both humidity and temperature + 0b00000000 // reserved + }; + + if (!this->write_bytes(HDC2010_CMD_CONFIGURATION_MEASUREMENT, data, 2)) { + ESP_LOGW(TAG, "Initial config instruction error"); + this->status_set_warning(); + return; + } + + // Set measurement mode to temperature and humidity + uint8_t config_contents; + this->read_register(MEASUREMENT_CONFIG, &config_contents, 1); + config_contents = (config_contents & 0xF9); // Always set to TEMP_AND_HUMID mode + this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1); + + // Set rate to manual + this->read_register(CONFIG, &config_contents, 1); + config_contents &= 0x8F; + this->write_bytes(CONFIG, &config_contents, 1); + + // Set temperature resolution to 14bit + this->read_register(CONFIG, &config_contents, 1); + config_contents &= 0x3F; + this->write_bytes(CONFIG, &config_contents, 1); + + // Set humidity resolution to 14bit + this->read_register(CONFIG, &config_contents, 1); + config_contents &= 0xCF; + this->write_bytes(CONFIG, &config_contents, 1); +} + +void HDC2010Component::dump_config() { + ESP_LOGCONFIG(TAG, "HDC2010:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +void HDC2010Component::update() { + // Trigger measurement + uint8_t config_contents; + this->read_register(CONFIG, &config_contents, 1); + config_contents |= 0x01; + this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1); + + // 1ms delay after triggering the sample + set_timeout(1, [this]() { + if (this->temperature_sensor_ != nullptr) { + float temp = this->read_temp(); + this->temperature_sensor_->publish_state(temp); + ESP_LOGD(TAG, "Temp=%.1f°C", temp); + } + + if (this->humidity_sensor_ != nullptr) { + float humidity = this->read_humidity(); + this->humidity_sensor_->publish_state(humidity); + ESP_LOGD(TAG, "Humidity=%.1f%%", humidity); + } + }); +} + +float HDC2010Component::read_temp() { + uint8_t byte[2]; + + this->read_register(HDC2010_CMD_TEMPERATURE_LOW, &byte[0], 1); + this->read_register(HDC2010_CMD_TEMPERATURE_HIGH, &byte[1], 1); + + uint16_t temp = encode_uint16(byte[1], byte[0]); + return (float) temp * 0.0025177f - 40.0f; +} + +float HDC2010Component::read_humidity() { + uint8_t byte[2]; + + this->read_register(HDC2010_CMD_HUMIDITY_LOW, &byte[0], 1); + this->read_register(HDC2010_CMD_HUMIDITY_HIGH, &byte[1], 1); + + uint16_t humidity = encode_uint16(byte[1], byte[0]); + return (float) humidity * 0.001525879f; +} + +} // namespace hdc2010 +} // namespace esphome diff --git a/esphome/components/hdc2010/hdc2010.h b/esphome/components/hdc2010/hdc2010.h new file mode 100644 index 0000000000..52c00686e6 --- /dev/null +++ b/esphome/components/hdc2010/hdc2010.h @@ -0,0 +1,32 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace hdc2010 { + +class HDC2010Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_temperature_sensor(sensor::Sensor *temperature) { this->temperature_sensor_ = temperature; } + + void set_humidity_sensor(sensor::Sensor *humidity) { this->humidity_sensor_ = humidity; } + + /// Setup the sensor and check for connection. + void setup() override; + void dump_config() override; + /// Retrieve the latest sensor values. This operation takes approximately 16ms. + void update() override; + + float read_temp(); + + float read_humidity(); + + protected: + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; +}; + +} // namespace hdc2010 +} // namespace esphome diff --git a/esphome/components/hdc2010/sensor.py b/esphome/components/hdc2010/sensor.py new file mode 100644 index 0000000000..15e19f2cc8 --- /dev/null +++ b/esphome/components/hdc2010/sensor.py @@ -0,0 +1,56 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +DEPENDENCIES = ["i2c"] + +hdc2010_ns = cg.esphome_ns.namespace("hdc2010") +HDC2010Component = hdc2010_ns.class_( + "HDC2010Component", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HDC2010Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x40)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity_sensor(sens)) diff --git a/tests/components/hdc2010/common.yaml b/tests/components/hdc2010/common.yaml new file mode 100644 index 0000000000..a22b3f15ce --- /dev/null +++ b/tests/components/hdc2010/common.yaml @@ -0,0 +1,7 @@ +sensor: + - platform: hdc2010 + i2c_id: i2c_bus + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/hdc2010/test.esp32-c3-idf.yaml b/tests/components/hdc2010/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..9990d96d29 --- /dev/null +++ b/tests/components/hdc2010/test.esp32-c3-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/hdc2010/test.esp32-idf.yaml b/tests/components/hdc2010/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/hdc2010/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/hdc2010/test.esp8266-ard.yaml b/tests/components/hdc2010/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/hdc2010/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/hdc2010/test.rp2040-ard.yaml b/tests/components/hdc2010/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/hdc2010/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml From d23e25f0991cb20900c0de5576218c3623d7a59e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 16:31:51 -1000 Subject: [PATCH 039/394] [api] Fix clang-tidy modernize-use-emplace warning for light effects (#11490) --- esphome/components/api/api_connection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 7c135946f8..f76080253d 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -486,7 +486,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c if (light->supports_effects()) { msg.effects.emplace_back("None"); for (auto *effect : light->get_effects()) { - msg.effects.push_back(effect->get_name()); + msg.effects.emplace_back(effect->get_name()); } } return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size, From 6c2ce5cacff9bf151642ee8c2185349de697c420 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Oct 2025 16:36:30 -1000 Subject: [PATCH 040/394] Bump bleak from 1.0.1 to 1.1.1 (#11492) Signed-off-by: dependabot[bot] 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 6966ebe583..59592ec0a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ pillow==11.3.0 cairosvg==2.8.2 freetype-py==2.5.1 jinja2==3.1.6 -bleak==1.0.1 +bleak==1.1.1 # esp-idf >= 5.0 requires this pyparsing >= 3.0 From 5b023f9369e3ce84e4f8b5717928bd9bbc1e735c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 16:37:50 -1000 Subject: [PATCH 041/394] [ethernet] Add RMII GPIO pin conflict validation (#11488) --- esphome/components/ethernet/__init__.py | 84 ++++++++++++++++++- tests/components/ethernet/common-dp83848.yaml | 4 +- tests/components/ethernet/common-ip101.yaml | 4 +- tests/components/ethernet/common-jl1101.yaml | 4 +- tests/components/ethernet/common-ksz8081.yaml | 4 +- .../ethernet/common-ksz8081rna.yaml | 4 +- tests/components/ethernet/common-lan8670.yaml | 4 +- tests/components/ethernet/common-lan8720.yaml | 4 +- tests/components/ethernet/common-rtl8201.yaml | 4 +- tests/components/ethernet_info/common.yaml | 4 +- 10 files changed, 98 insertions(+), 22 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 7384bb26d3..77f70a3630 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -32,6 +32,7 @@ from esphome.const import ( CONF_MISO_PIN, CONF_MODE, CONF_MOSI_PIN, + CONF_NUMBER, CONF_PAGE_ID, CONF_PIN, CONF_POLLING_INTERVAL, @@ -52,12 +53,36 @@ from esphome.core import ( coroutine_with_priority, ) import esphome.final_validate as fv +from esphome.types import ConfigType CONFLICTS_WITH = ["wifi"] DEPENDENCIES = ["esp32"] AUTO_LOAD = ["network"] LOGGER = logging.getLogger(__name__) +# RMII pins that are hardcoded on ESP32 classic and cannot be changed +# These pins are used by the internal Ethernet MAC when using RMII PHYs +ESP32_RMII_FIXED_PINS = { + 19: "EMAC_TXD0", + 21: "EMAC_TX_EN", + 22: "EMAC_TXD1", + 25: "EMAC_RXD0", + 26: "EMAC_RXD1", + 27: "EMAC_RX_CRS_DV", +} + +# RMII default pins for ESP32-P4 +# These are the default pins used by ESP-IDF and are configurable in principle, +# but ESPHome's ethernet component currently has no way to change them +ESP32P4_RMII_DEFAULT_PINS = { + 34: "EMAC_TXD0", + 35: "EMAC_TXD1", + 28: "EMAC_RX_CRS_DV", + 29: "EMAC_RXD0", + 30: "EMAC_RXD1", + 49: "EMAC_TX_EN", +} + ethernet_ns = cg.esphome_ns.namespace("ethernet") PHYRegister = ethernet_ns.struct("PHYRegister") CONF_PHY_ADDR = "phy_addr" @@ -273,7 +298,7 @@ CONFIG_SCHEMA = cv.All( ) -def _final_validate(config): +def _final_validate_spi(config): if config[CONF_TYPE] not in SPI_ETHERNET_TYPES: return if spi_configs := fv.full_config.get().get(CONF_SPI): @@ -292,9 +317,6 @@ def _final_validate(config): ) -FINAL_VALIDATE_SCHEMA = _final_validate - - def manual_ip(config): return cg.StructInitializer( ManualIP, @@ -383,3 +405,57 @@ async def to_code(config): if CORE.using_arduino: cg.add_library("WiFi", None) + + +def _final_validate_rmii_pins(config: ConfigType) -> None: + """Validate that RMII pins are not used by other components.""" + # Only validate for RMII-based PHYs on ESP32/ESP32P4 + if config[CONF_TYPE] in SPI_ETHERNET_TYPES or config[CONF_TYPE] == "OPENETH": + return # SPI and OPENETH don't use RMII + + variant = get_esp32_variant() + if variant == VARIANT_ESP32: + rmii_pins = ESP32_RMII_FIXED_PINS + is_configurable = False + elif variant == VARIANT_ESP32P4: + rmii_pins = ESP32P4_RMII_DEFAULT_PINS + is_configurable = True + else: + return # No RMII validation needed for other variants + + # Check all used pins against RMII reserved pins + for pin_list in pins.PIN_SCHEMA_REGISTRY.pins_used.values(): + for pin_path, _, pin_config in pin_list: + pin_num = pin_config.get(CONF_NUMBER) + if pin_num not in rmii_pins: + continue + # Found a conflict - show helpful error message + pin_function = rmii_pins[pin_num] + component_path = ".".join(str(p) for p in pin_path) + if is_configurable: + error_msg = ( + f"GPIO{pin_num} is used by Ethernet RMII " + f"({pin_function}) with the current default " + f"configuration. This conflicts with '{component_path}'. " + f"Please choose a different GPIO pin for " + f"'{component_path}'." + ) + else: + error_msg = ( + f"GPIO{pin_num} is reserved for Ethernet RMII " + f"({pin_function}) and cannot be used. This pin is " + f"hardcoded by ESP-IDF and cannot be changed when using " + f"RMII Ethernet PHYs. Please choose a different GPIO pin " + f"for '{component_path}'." + ) + raise cv.Invalid(error_msg, path=pin_path) + + +def _final_validate(config: ConfigType) -> ConfigType: + """Final validation for Ethernet component.""" + _final_validate_spi(config) + _final_validate_rmii_pins(config) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate diff --git a/tests/components/ethernet/common-dp83848.yaml b/tests/components/ethernet/common-dp83848.yaml index 7cedfeaf08..f9069c5fb9 100644 --- a/tests/components/ethernet/common-dp83848.yaml +++ b/tests/components/ethernet/common-dp83848.yaml @@ -1,12 +1,12 @@ ethernet: type: DP83848 mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 diff --git a/tests/components/ethernet/common-ip101.yaml b/tests/components/ethernet/common-ip101.yaml index 2dece15171..cea7a5cc35 100644 --- a/tests/components/ethernet/common-ip101.yaml +++ b/tests/components/ethernet/common-ip101.yaml @@ -1,12 +1,12 @@ ethernet: type: IP101 mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 diff --git a/tests/components/ethernet/common-jl1101.yaml b/tests/components/ethernet/common-jl1101.yaml index b6ea884102..7b0a2dfdc4 100644 --- a/tests/components/ethernet/common-jl1101.yaml +++ b/tests/components/ethernet/common-jl1101.yaml @@ -1,12 +1,12 @@ ethernet: type: JL1101 mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 diff --git a/tests/components/ethernet/common-ksz8081.yaml b/tests/components/ethernet/common-ksz8081.yaml index f70d42319e..65541832c2 100644 --- a/tests/components/ethernet/common-ksz8081.yaml +++ b/tests/components/ethernet/common-ksz8081.yaml @@ -1,12 +1,12 @@ ethernet: type: KSZ8081 mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 diff --git a/tests/components/ethernet/common-ksz8081rna.yaml b/tests/components/ethernet/common-ksz8081rna.yaml index 18efdae0e1..f04cba15b2 100644 --- a/tests/components/ethernet/common-ksz8081rna.yaml +++ b/tests/components/ethernet/common-ksz8081rna.yaml @@ -1,12 +1,12 @@ ethernet: type: KSZ8081RNA mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 diff --git a/tests/components/ethernet/common-lan8670.yaml b/tests/components/ethernet/common-lan8670.yaml index ec2f24273d..fb751ebd23 100644 --- a/tests/components/ethernet/common-lan8670.yaml +++ b/tests/components/ethernet/common-lan8670.yaml @@ -1,12 +1,12 @@ ethernet: type: LAN8670 mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 diff --git a/tests/components/ethernet/common-lan8720.yaml b/tests/components/ethernet/common-lan8720.yaml index 204c1d9210..838d57df28 100644 --- a/tests/components/ethernet/common-lan8720.yaml +++ b/tests/components/ethernet/common-lan8720.yaml @@ -1,12 +1,12 @@ ethernet: type: LAN8720 mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 diff --git a/tests/components/ethernet/common-rtl8201.yaml b/tests/components/ethernet/common-rtl8201.yaml index 8b9f2b86f2..0e7cbe73c6 100644 --- a/tests/components/ethernet/common-rtl8201.yaml +++ b/tests/components/ethernet/common-rtl8201.yaml @@ -1,12 +1,12 @@ ethernet: type: RTL8201 mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 diff --git a/tests/components/ethernet_info/common.yaml b/tests/components/ethernet_info/common.yaml index f45f345316..b720521d10 100644 --- a/tests/components/ethernet_info/common.yaml +++ b/tests/components/ethernet_info/common.yaml @@ -1,12 +1,12 @@ ethernet: type: LAN8720 mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 From 3d21adecd335e683b695ccfc00175da442214893 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 02:58:09 +0000 Subject: [PATCH 042/394] Bump aioesphomeapi from 42.2.0 to 42.3.0 (#11493) 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 59592ec0a2..351143591a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.1.0 click==8.1.7 esphome-dashboard==20251013.0 -aioesphomeapi==42.2.0 +aioesphomeapi==42.3.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.18.15 # dashboard_import From 917deac7cb7d6747f06a3c9546be85b3433e29b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 18:02:19 -1000 Subject: [PATCH 043/394] [scheduler] Remove unused include after defer queue optimization (#11491) --- esphome/core/scheduler.h | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index ad0ec0284e..df0be0e4ce 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -4,7 +4,6 @@ #include #include #include -#include #ifdef ESPHOME_THREAD_MULTI_ATOMICS #include #endif From a89511f3ae9a4f6a9b81ffe53ef7030aa41c7628 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 19:00:58 -1000 Subject: [PATCH 044/394] [http_request] Pass collect_headers by const reference instead of by value --- esphome/components/http_request/http_request.h | 2 +- esphome/components/http_request/http_request_arduino.cpp | 2 +- esphome/components/http_request/http_request_arduino.h | 2 +- esphome/components/http_request/http_request_host.cpp | 2 +- esphome/components/http_request/http_request_host.h | 2 +- esphome/components/http_request/http_request_idf.cpp | 2 +- esphome/components/http_request/http_request_idf.h | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index bb14cc6f51..40c85d51ed 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -169,7 +169,7 @@ class HttpRequestComponent : public Component { protected: virtual std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - std::set collect_headers) = 0; + const std::set &collect_headers) = 0; const char *useragent_{nullptr}; bool follow_redirects_{}; uint16_t redirect_limit_{}; diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index dfdbbd3fab..c64a7be554 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -17,7 +17,7 @@ static const char *const TAG = "http_request.arduino"; std::shared_ptr HttpRequestArduino::perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - std::set collect_headers) { + const std::set &collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index c8208c74d8..b736bb56d1 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -33,7 +33,7 @@ class HttpRequestArduino : public HttpRequestComponent { protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - std::set collect_headers) override; + const std::set &collect_headers) override; }; } // namespace http_request diff --git a/esphome/components/http_request/http_request_host.cpp b/esphome/components/http_request/http_request_host.cpp index c20ea552b7..402affc1d1 100644 --- a/esphome/components/http_request/http_request_host.cpp +++ b/esphome/components/http_request/http_request_host.cpp @@ -20,7 +20,7 @@ static const char *const TAG = "http_request.host"; std::shared_ptr HttpRequestHost::perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - std::set response_headers) { + const std::set &response_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); diff --git a/esphome/components/http_request/http_request_host.h b/esphome/components/http_request/http_request_host.h index fdd72e7ea5..886ba94938 100644 --- a/esphome/components/http_request/http_request_host.h +++ b/esphome/components/http_request/http_request_host.h @@ -20,7 +20,7 @@ class HttpRequestHost : public HttpRequestComponent { public: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - std::set response_headers) override; + const std::set &response_headers) override; void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; } protected: diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index a91c0bfc25..34a3fb87eb 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -55,7 +55,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { std::shared_ptr HttpRequestIDF::perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - std::set collect_headers) { + const std::set &collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); ESP_LOGE(TAG, "HTTP Request failed; Not connected to network"); diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index 90dee0be68..e51b3aaebc 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -39,7 +39,7 @@ class HttpRequestIDF : public HttpRequestComponent { protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - std::set collect_headers) override; + const std::set &collect_headers) override; // if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE uint16_t buffer_size_rx_{}; uint16_t buffer_size_tx_{}; From b61cc2003fbe547bd0b209d4d61f2a473d47a4df Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 19:49:27 -1000 Subject: [PATCH 045/394] [core][sensor] Eliminate redundant default value setters in generated code --- esphome/components/sensor/__init__.py | 4 +++- esphome/core/entity_helpers.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 7e91bb83c4..93283e4d47 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -878,7 +878,9 @@ async def setup_sensor_core_(var, config): cg.add(var.set_unit_of_measurement(unit_of_measurement)) if (accuracy_decimals := config.get(CONF_ACCURACY_DECIMALS)) is not None: cg.add(var.set_accuracy_decimals(accuracy_decimals)) - cg.add(var.set_force_update(config[CONF_FORCE_UPDATE])) + # Only set force_update if True (default is False) + if config[CONF_FORCE_UPDATE]: + cg.add(var.set_force_update(True)) if config.get(CONF_FILTERS): # must exist and not be empty filters = await build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index f0a04b4860..9b4786f835 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -105,7 +105,9 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None: config[CONF_NAME], platform, ) - add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) + # Only set disabled_by_default if True (default is False) + if config[CONF_DISABLED_BY_DEFAULT]: + add(var.set_disabled_by_default(True)) if CONF_INTERNAL in config: add(var.set_internal(config[CONF_INTERNAL])) if CONF_ICON in config: From f9b08491cc54d8fe24f3f5ce1a25ee257f5d0d09 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Oct 2025 06:50:24 -0600 Subject: [PATCH 046/394] [tests] Fix millis() ambiguity in component tests with gps component --- tests/components/absolute_humidity/common.yaml | 4 ++-- tests/components/analog_threshold/common.yaml | 2 +- tests/components/bang_bang/common.yaml | 2 +- tests/components/binary_sensor_map/common.yaml | 6 +++--- tests/components/combination/common.yaml | 4 ++-- tests/components/duty_time/common.yaml | 2 +- tests/components/endstop/common.yaml | 2 +- tests/components/lock/common.yaml | 2 +- tests/components/pid/common.yaml | 2 +- tests/components/prometheus/common.yaml | 12 ++++++------ 10 files changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/components/absolute_humidity/common.yaml b/tests/components/absolute_humidity/common.yaml index 026f88654f..f6b1c02886 100644 --- a/tests/components/absolute_humidity/common.yaml +++ b/tests/components/absolute_humidity/common.yaml @@ -6,14 +6,14 @@ sensor: - platform: template id: template_humidity lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return 0.6; } return 0.0; - platform: template id: template_temperature lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return 42.0; } return 0.0; diff --git a/tests/components/analog_threshold/common.yaml b/tests/components/analog_threshold/common.yaml index 26c401b92a..7d9dc4bc9b 100644 --- a/tests/components/analog_threshold/common.yaml +++ b/tests/components/analog_threshold/common.yaml @@ -3,7 +3,7 @@ sensor: id: template_sensor name: Template Sensor lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return 42.0; } return 0.0; diff --git a/tests/components/bang_bang/common.yaml b/tests/components/bang_bang/common.yaml index 5882025191..dc7798e2f2 100644 --- a/tests/components/bang_bang/common.yaml +++ b/tests/components/bang_bang/common.yaml @@ -10,7 +10,7 @@ sensor: - platform: template id: template_sensor1 lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return 42.0; } else { return 0.0; diff --git a/tests/components/binary_sensor_map/common.yaml b/tests/components/binary_sensor_map/common.yaml index c054022583..71f4c0158e 100644 --- a/tests/components/binary_sensor_map/common.yaml +++ b/tests/components/binary_sensor_map/common.yaml @@ -2,21 +2,21 @@ binary_sensor: - platform: template id: bin1 lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return true; } return false; - platform: template id: bin2 lambda: |- - if (millis() > 20000) { + if (esphome::millis() > 20000) { return true; } return false; - platform: template id: bin3 lambda: |- - if (millis() > 30000) { + if (esphome::millis() > 30000) { return true; } return false; diff --git a/tests/components/combination/common.yaml b/tests/components/combination/common.yaml index 0e5d512d08..bb05fa375b 100644 --- a/tests/components/combination/common.yaml +++ b/tests/components/combination/common.yaml @@ -2,14 +2,14 @@ sensor: - platform: template id: template_temperature1 lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return 0.6; } return 0.0; - platform: template id: template_temperature2 lambda: |- - if (millis() > 20000) { + if (esphome::millis() > 20000) { return 0.8; } return 0.0; diff --git a/tests/components/duty_time/common.yaml b/tests/components/duty_time/common.yaml index 761d10f16a..ffe62ec7fc 100644 --- a/tests/components/duty_time/common.yaml +++ b/tests/components/duty_time/common.yaml @@ -2,7 +2,7 @@ binary_sensor: - platform: template id: bin1 lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return true; } return false; diff --git a/tests/components/endstop/common.yaml b/tests/components/endstop/common.yaml index b92b1e13b9..317b31b1cb 100644 --- a/tests/components/endstop/common.yaml +++ b/tests/components/endstop/common.yaml @@ -2,7 +2,7 @@ binary_sensor: - platform: template id: bin1 lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return true; } return false; diff --git a/tests/components/lock/common.yaml b/tests/components/lock/common.yaml index 9ba7f34857..35907fe679 100644 --- a/tests/components/lock/common.yaml +++ b/tests/components/lock/common.yaml @@ -15,7 +15,7 @@ lock: id: test_lock1 name: Template Lock lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return LOCK_STATE_LOCKED; } return LOCK_STATE_UNLOCKED; diff --git a/tests/components/pid/common.yaml b/tests/components/pid/common.yaml index 262e75591e..e9478103f6 100644 --- a/tests/components/pid/common.yaml +++ b/tests/components/pid/common.yaml @@ -25,7 +25,7 @@ sensor: - platform: template id: template_sensor1 lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return 42.0; } return 0.0; diff --git a/tests/components/prometheus/common.yaml b/tests/components/prometheus/common.yaml index cf46e882a7..f9bd471ce7 100644 --- a/tests/components/prometheus/common.yaml +++ b/tests/components/prometheus/common.yaml @@ -33,7 +33,7 @@ sensor: - platform: template id: template_sensor1 lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return 42.0; } return 0.0; @@ -46,7 +46,7 @@ text_sensor: - platform: template id: template_text_sensor1 lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return {"Hello World"}; } return {"Goodbye (cruel) World"}; @@ -56,7 +56,7 @@ binary_sensor: - platform: template id: template_binary_sensor1 lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return true; } return false; @@ -65,7 +65,7 @@ switch: - platform: template id: template_switch1 lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return true; } return false; @@ -79,7 +79,7 @@ cover: - platform: template id: template_cover1 lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return COVER_OPEN; } return COVER_CLOSED; @@ -88,7 +88,7 @@ lock: - platform: template id: template_lock1 lambda: |- - if (millis() > 10000) { + if (esphome::millis() > 10000) { return LOCK_STATE_LOCKED; } return LOCK_STATE_UNLOCKED; From cce5b58de46a18f84f95160590064d82788b7f35 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Oct 2025 08:19:48 -0600 Subject: [PATCH 047/394] Revert "[tests] Fix millis() ambiguity in component tests with gps component" This reverts commit f9b08491cc54d8fe24f3f5ce1a25ee257f5d0d09. --- tests/components/absolute_humidity/common.yaml | 4 ++-- tests/components/analog_threshold/common.yaml | 2 +- tests/components/bang_bang/common.yaml | 2 +- tests/components/binary_sensor_map/common.yaml | 6 +++--- tests/components/combination/common.yaml | 4 ++-- tests/components/duty_time/common.yaml | 2 +- tests/components/endstop/common.yaml | 2 +- tests/components/lock/common.yaml | 2 +- tests/components/pid/common.yaml | 2 +- tests/components/prometheus/common.yaml | 12 ++++++------ 10 files changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/components/absolute_humidity/common.yaml b/tests/components/absolute_humidity/common.yaml index f6b1c02886..026f88654f 100644 --- a/tests/components/absolute_humidity/common.yaml +++ b/tests/components/absolute_humidity/common.yaml @@ -6,14 +6,14 @@ sensor: - platform: template id: template_humidity lambda: |- - if (esphome::millis() > 10000) { + if (millis() > 10000) { return 0.6; } return 0.0; - platform: template id: template_temperature lambda: |- - if (esphome::millis() > 10000) { + if (millis() > 10000) { return 42.0; } return 0.0; diff --git a/tests/components/analog_threshold/common.yaml b/tests/components/analog_threshold/common.yaml index 7d9dc4bc9b..26c401b92a 100644 --- a/tests/components/analog_threshold/common.yaml +++ b/tests/components/analog_threshold/common.yaml @@ -3,7 +3,7 @@ sensor: id: template_sensor name: Template Sensor lambda: |- - if (esphome::millis() > 10000) { + if (millis() > 10000) { return 42.0; } return 0.0; diff --git a/tests/components/bang_bang/common.yaml b/tests/components/bang_bang/common.yaml index dc7798e2f2..5882025191 100644 --- a/tests/components/bang_bang/common.yaml +++ b/tests/components/bang_bang/common.yaml @@ -10,7 +10,7 @@ sensor: - platform: template id: template_sensor1 lambda: |- - if (esphome::millis() > 10000) { + if (millis() > 10000) { return 42.0; } else { return 0.0; diff --git a/tests/components/binary_sensor_map/common.yaml b/tests/components/binary_sensor_map/common.yaml index 71f4c0158e..c054022583 100644 --- a/tests/components/binary_sensor_map/common.yaml +++ b/tests/components/binary_sensor_map/common.yaml @@ -2,21 +2,21 @@ binary_sensor: - platform: template id: bin1 lambda: |- - if (esphome::millis() > 10000) { + if (millis() > 10000) { return true; } return false; - platform: template id: bin2 lambda: |- - if (esphome::millis() > 20000) { + if (millis() > 20000) { return true; } return false; - platform: template id: bin3 lambda: |- - if (esphome::millis() > 30000) { + if (millis() > 30000) { return true; } return false; diff --git a/tests/components/combination/common.yaml b/tests/components/combination/common.yaml index bb05fa375b..0e5d512d08 100644 --- a/tests/components/combination/common.yaml +++ b/tests/components/combination/common.yaml @@ -2,14 +2,14 @@ sensor: - platform: template id: template_temperature1 lambda: |- - if (esphome::millis() > 10000) { + if (millis() > 10000) { return 0.6; } return 0.0; - platform: template id: template_temperature2 lambda: |- - if (esphome::millis() > 20000) { + if (millis() > 20000) { return 0.8; } return 0.0; diff --git a/tests/components/duty_time/common.yaml b/tests/components/duty_time/common.yaml index ffe62ec7fc..761d10f16a 100644 --- a/tests/components/duty_time/common.yaml +++ b/tests/components/duty_time/common.yaml @@ -2,7 +2,7 @@ binary_sensor: - platform: template id: bin1 lambda: |- - if (esphome::millis() > 10000) { + if (millis() > 10000) { return true; } return false; diff --git a/tests/components/endstop/common.yaml b/tests/components/endstop/common.yaml index 317b31b1cb..b92b1e13b9 100644 --- a/tests/components/endstop/common.yaml +++ b/tests/components/endstop/common.yaml @@ -2,7 +2,7 @@ binary_sensor: - platform: template id: bin1 lambda: |- - if (esphome::millis() > 10000) { + if (millis() > 10000) { return true; } return false; diff --git a/tests/components/lock/common.yaml b/tests/components/lock/common.yaml index 35907fe679..9ba7f34857 100644 --- a/tests/components/lock/common.yaml +++ b/tests/components/lock/common.yaml @@ -15,7 +15,7 @@ lock: id: test_lock1 name: Template Lock lambda: |- - if (esphome::millis() > 10000) { + if (millis() > 10000) { return LOCK_STATE_LOCKED; } return LOCK_STATE_UNLOCKED; diff --git a/tests/components/pid/common.yaml b/tests/components/pid/common.yaml index e9478103f6..262e75591e 100644 --- a/tests/components/pid/common.yaml +++ b/tests/components/pid/common.yaml @@ -25,7 +25,7 @@ sensor: - platform: template id: template_sensor1 lambda: |- - if (esphome::millis() > 10000) { + if (millis() > 10000) { return 42.0; } return 0.0; diff --git a/tests/components/prometheus/common.yaml b/tests/components/prometheus/common.yaml index f9bd471ce7..cf46e882a7 100644 --- a/tests/components/prometheus/common.yaml +++ b/tests/components/prometheus/common.yaml @@ -33,7 +33,7 @@ sensor: - platform: template id: template_sensor1 lambda: |- - if (esphome::millis() > 10000) { + if (millis() > 10000) { return 42.0; } return 0.0; @@ -46,7 +46,7 @@ text_sensor: - platform: template id: template_text_sensor1 lambda: |- - if (esphome::millis() > 10000) { + if (millis() > 10000) { return {"Hello World"}; } return {"Goodbye (cruel) World"}; @@ -56,7 +56,7 @@ binary_sensor: - platform: template id: template_binary_sensor1 lambda: |- - if (esphome::millis() > 10000) { + if (millis() > 10000) { return true; } return false; @@ -65,7 +65,7 @@ switch: - platform: template id: template_switch1 lambda: |- - if (esphome::millis() > 10000) { + if (millis() > 10000) { return true; } return false; @@ -79,7 +79,7 @@ cover: - platform: template id: template_cover1 lambda: |- - if (esphome::millis() > 10000) { + if (millis() > 10000) { return COVER_OPEN; } return COVER_CLOSED; @@ -88,7 +88,7 @@ lock: - platform: template id: template_lock1 lambda: |- - if (esphome::millis() > 10000) { + if (millis() > 10000) { return LOCK_STATE_LOCKED; } return LOCK_STATE_UNLOCKED; From fdd453e88ac72475950d848f2bedaa4593ace8c5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Oct 2025 09:02:08 -0600 Subject: [PATCH 048/394] fix --- script/analyze_component_buses.py | 1 + 1 file changed, 1 insertion(+) diff --git a/script/analyze_component_buses.py b/script/analyze_component_buses.py index 78f5ca3344..38d1f8c2b7 100755 --- a/script/analyze_component_buses.py +++ b/script/analyze_component_buses.py @@ -77,6 +77,7 @@ ISOLATED_COMPONENTS = { "esphome": "Defines devices/areas in esphome: section that are referenced in other sections - breaks when merged", "ethernet": "Defines ethernet: which conflicts with wifi: used by most components", "ethernet_info": "Related to ethernet component which conflicts with wifi", + "gps": "TinyGPSPlus library declares millis() function that creates ambiguity with ESPHome millis() macro when merged with components using millis() in lambdas", "lvgl": "Defines multiple SDL displays on host platform that conflict when merged with other display configs", "mapping": "Uses dict format for image/display sections incompatible with standard list format - ESPHome merge_config cannot handle", "openthread": "Conflicts with wifi: used by most components", From ab14c0cd723ccd9979cf30fed7fa2d12eedf1328 Mon Sep 17 00:00:00 2001 From: Patrick Date: Thu, 23 Oct 2025 17:32:02 +0200 Subject: [PATCH 049/394] [pipsolar] improve sensor readout in HA, set unknown state on timeout / error (#10292) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/pipsolar/pipsolar.cpp | 97 +++++++++---------- esphome/components/pipsolar/pipsolar.h | 3 + .../components/pipsolar/sensor/__init__.py | 96 +++++++++++++----- 3 files changed, 120 insertions(+), 76 deletions(-) diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index b92cc3be9f..bafd5273da 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -38,7 +38,6 @@ void Pipsolar::loop() { } if (this->state_ == STATE_COMMAND_COMPLETE) { if (this->check_incoming_length_(4)) { - ESP_LOGD(TAG, "response length for command OK"); if (this->check_incoming_crc_()) { // crc ok if (this->read_buffer_[1] == 'A' && this->read_buffer_[2] == 'C' && this->read_buffer_[3] == 'K') { @@ -49,15 +48,15 @@ void Pipsolar::loop() { this->command_queue_[this->command_queue_position_] = std::string(""); this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; this->state_ = STATE_IDLE; - } else { // crc failed + // no log message necessary, check_incoming_crc_() logs this->command_queue_[this->command_queue_position_] = std::string(""); this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; this->state_ = STATE_IDLE; } } else { - ESP_LOGD(TAG, "response length for command %s not OK: with length %zu", + ESP_LOGD(TAG, "command %s response length not OK: with length %zu", this->command_queue_[this->command_queue_position_].c_str(), this->read_pos_); this->command_queue_[this->command_queue_position_] = std::string(""); this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; @@ -66,46 +65,10 @@ void Pipsolar::loop() { } if (this->state_ == STATE_POLL_CHECKED) { - switch (this->enabled_polling_commands_[this->last_polling_command_].identifier) { - case POLLING_QPIRI: - ESP_LOGD(TAG, "Decode QPIRI"); - handle_qpiri_((const char *) this->read_buffer_); - this->state_ = STATE_IDLE; - break; - case POLLING_QPIGS: - ESP_LOGD(TAG, "Decode QPIGS"); - handle_qpigs_((const char *) this->read_buffer_); - this->state_ = STATE_IDLE; - break; - case POLLING_QMOD: - ESP_LOGD(TAG, "Decode QMOD"); - handle_qmod_((const char *) this->read_buffer_); - this->state_ = STATE_IDLE; - break; - case POLLING_QFLAG: - ESP_LOGD(TAG, "Decode QFLAG"); - handle_qflag_((const char *) this->read_buffer_); - this->state_ = STATE_IDLE; - break; - case POLLING_QPIWS: - ESP_LOGD(TAG, "Decode QPIWS"); - handle_qpiws_((const char *) this->read_buffer_); - this->state_ = STATE_IDLE; - break; - case POLLING_QT: - ESP_LOGD(TAG, "Decode QT"); - handle_qt_((const char *) this->read_buffer_); - this->state_ = STATE_IDLE; - break; - case POLLING_QMN: - ESP_LOGD(TAG, "Decode QMN"); - handle_qmn_((const char *) this->read_buffer_); - this->state_ = STATE_IDLE; - break; - default: - this->state_ = STATE_IDLE; - break; - } + ESP_LOGD(TAG, "poll %s decode", this->enabled_polling_commands_[this->last_polling_command_].command); + this->handle_poll_response_(this->enabled_polling_commands_[this->last_polling_command_].identifier, + (const char *) this->read_buffer_); + this->state_ = STATE_IDLE; return; } @@ -113,6 +76,8 @@ void Pipsolar::loop() { if (this->check_incoming_crc_()) { if (this->read_buffer_[0] == '(' && this->read_buffer_[1] == 'N' && this->read_buffer_[2] == 'A' && this->read_buffer_[3] == 'K') { + ESP_LOGD(TAG, "poll %s NACK", this->enabled_polling_commands_[this->last_polling_command_].command); + this->handle_poll_error_(this->enabled_polling_commands_[this->last_polling_command_].identifier); this->state_ = STATE_IDLE; return; } @@ -121,6 +86,9 @@ void Pipsolar::loop() { this->state_ = STATE_POLL_CHECKED; return; } else { + // crc failed + // no log message necessary, check_incoming_crc_() logs + this->handle_poll_error_(this->enabled_polling_commands_[this->last_polling_command_].identifier); this->state_ = STATE_IDLE; } } @@ -158,21 +126,19 @@ void Pipsolar::loop() { // command timeout const char *command = this->command_queue_[this->command_queue_position_].c_str(); this->command_start_millis_ = millis(); - ESP_LOGD(TAG, "timeout command from queue: %s", command); + ESP_LOGD(TAG, "command %s timeout", command); this->command_queue_[this->command_queue_position_] = std::string(""); this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; this->state_ = STATE_IDLE; return; - } else { } } if (this->state_ == STATE_POLL) { if (millis() - this->command_start_millis_ > esphome::pipsolar::Pipsolar::COMMAND_TIMEOUT) { // command timeout - ESP_LOGD(TAG, "timeout command to poll: %s", - this->enabled_polling_commands_[this->last_polling_command_].command); + ESP_LOGD(TAG, "poll %s timeout", this->enabled_polling_commands_[this->last_polling_command_].command); + this->handle_poll_error_(this->enabled_polling_commands_[this->last_polling_command_].identifier); this->state_ = STATE_IDLE; - } else { } } } @@ -187,7 +153,6 @@ uint8_t Pipsolar::check_incoming_length_(uint8_t length) { uint8_t Pipsolar::check_incoming_crc_() { uint16_t crc16; crc16 = this->pipsolar_crc_(read_buffer_, read_pos_ - 3); - ESP_LOGD(TAG, "checking crc on incoming message"); if (((uint8_t) ((crc16) >> 8)) == read_buffer_[read_pos_ - 3] && ((uint8_t) ((crc16) &0xff)) == read_buffer_[read_pos_ - 2]) { ESP_LOGD(TAG, "CRC OK"); @@ -253,7 +218,7 @@ bool Pipsolar::send_next_poll_() { this->write(((uint8_t) ((crc16) &0xff))); // lowbyte // end Byte this->write(0x0D); - ESP_LOGD(TAG, "Sending polling command : %s with length %d", + ESP_LOGD(TAG, "Sending polling command: %s with length %d", this->enabled_polling_commands_[this->last_polling_command_].command, this->enabled_polling_commands_[this->last_polling_command_].length); return true; @@ -274,6 +239,38 @@ void Pipsolar::queue_command(const std::string &command) { ESP_LOGD(TAG, "Command queue full dropping command: %s", command.c_str()); } +void Pipsolar::handle_poll_response_(ENUMPollingCommand polling_command, const char *message) { + switch (polling_command) { + case POLLING_QPIRI: + handle_qpiri_(message); + break; + case POLLING_QPIGS: + handle_qpigs_(message); + break; + case POLLING_QMOD: + handle_qmod_(message); + break; + case POLLING_QFLAG: + handle_qflag_(message); + break; + case POLLING_QPIWS: + handle_qpiws_(message); + break; + case POLLING_QT: + handle_qt_(message); + break; + case POLLING_QMN: + handle_qmn_(message); + break; + default: + break; + } +} +void Pipsolar::handle_poll_error_(ENUMPollingCommand polling_command) { + // handlers are designed in a way that an empty message sets all sensors to unknown + this->handle_poll_response_(polling_command, ""); +} + void Pipsolar::handle_qpiri_(const char *message) { if (this->last_qpiri_) { this->last_qpiri_->publish_state(message); diff --git a/esphome/components/pipsolar/pipsolar.h b/esphome/components/pipsolar/pipsolar.h index 40056bac9d..beae67a4e0 100644 --- a/esphome/components/pipsolar/pipsolar.h +++ b/esphome/components/pipsolar/pipsolar.h @@ -204,6 +204,9 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent { bool send_next_command_(); bool send_next_poll_(); + void handle_poll_response_(ENUMPollingCommand polling_command, const char *message); + void handle_poll_error_(ENUMPollingCommand polling_command); + // these handlers are designed in a way that an empty message sets all sensors to unknown void handle_qpiri_(const char *message); void handle_qpigs_(const char *message); void handle_qmod_(const char *message); diff --git a/esphome/components/pipsolar/sensor/__init__.py b/esphome/components/pipsolar/sensor/__init__.py index 929865b480..d08a877b55 100644 --- a/esphome/components/pipsolar/sensor/__init__.py +++ b/esphome/components/pipsolar/sensor/__init__.py @@ -4,11 +4,18 @@ import esphome.config_validation as cv from esphome.const import ( CONF_BATTERY_VOLTAGE, CONF_BUS_VOLTAGE, + DEVICE_CLASS_APPARENT_POWER, + DEVICE_CLASS_BATTERY, DEVICE_CLASS_CURRENT, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, + ICON_BATTERY, ICON_CURRENT_AC, + ICON_FLASH, + ICON_GAUGE, + STATE_CLASS_MEASUREMENT, UNIT_AMPERE, UNIT_CELSIUS, UNIT_HERTZ, @@ -22,6 +29,10 @@ from .. import CONF_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA DEPENDENCIES = ["uart"] +ICON_SOLAR_POWER = "mdi:solar-power" +ICON_SOLAR_PANEL = "mdi:solar-panel" +ICON_CURRENT_DC = "mdi:current-dc" + # QPIRI sensors CONF_GRID_RATING_VOLTAGE = "grid_rating_voltage" CONF_GRID_RATING_CURRENT = "grid_rating_current" @@ -75,16 +86,19 @@ TYPES = { unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_GRID_RATING_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, accuracy_decimals=1, device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_AC_OUTPUT_RATING_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_AC_OUTPUT_RATING_FREQUENCY: sensor.sensor_schema( unit_of_measurement=UNIT_HERTZ, @@ -98,11 +112,12 @@ TYPES = { ), CONF_AC_OUTPUT_RATING_APPARENT_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS, - accuracy_decimals=1, + accuracy_decimals=0, + device_class=DEVICE_CLASS_APPARENT_POWER, ), CONF_AC_OUTPUT_RATING_ACTIVE_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_WATT, - accuracy_decimals=1, + accuracy_decimals=0, device_class=DEVICE_CLASS_POWER, ), CONF_BATTERY_RATING_VOLTAGE: sensor.sensor_schema( @@ -131,124 +146,151 @@ TYPES = { device_class=DEVICE_CLASS_VOLTAGE, ), CONF_BATTERY_TYPE: sensor.sensor_schema( - accuracy_decimals=1, + accuracy_decimals=0, ), CONF_CURRENT_MAX_AC_CHARGING_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, - accuracy_decimals=1, + accuracy_decimals=0, device_class=DEVICE_CLASS_CURRENT, ), CONF_CURRENT_MAX_CHARGING_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, - accuracy_decimals=1, + accuracy_decimals=0, device_class=DEVICE_CLASS_CURRENT, ), CONF_INPUT_VOLTAGE_RANGE: sensor.sensor_schema( - accuracy_decimals=1, + accuracy_decimals=0, ), CONF_OUTPUT_SOURCE_PRIORITY: sensor.sensor_schema( - accuracy_decimals=1, + accuracy_decimals=0, ), CONF_CHARGER_SOURCE_PRIORITY: sensor.sensor_schema( - accuracy_decimals=1, + accuracy_decimals=0, ), CONF_PARALLEL_MAX_NUM: sensor.sensor_schema( - accuracy_decimals=1, + accuracy_decimals=0, ), CONF_MACHINE_TYPE: sensor.sensor_schema( - accuracy_decimals=1, + accuracy_decimals=0, ), CONF_TOPOLOGY: sensor.sensor_schema( - accuracy_decimals=1, + accuracy_decimals=0, ), CONF_OUTPUT_MODE: sensor.sensor_schema( - accuracy_decimals=1, + accuracy_decimals=0, ), CONF_BATTERY_REDISCHARGE_VOLTAGE: sensor.sensor_schema( accuracy_decimals=1, ), CONF_PV_OK_CONDITION_FOR_PARALLEL: sensor.sensor_schema( - accuracy_decimals=1, + accuracy_decimals=0, ), CONF_PV_POWER_BALANCE: sensor.sensor_schema( - accuracy_decimals=1, + accuracy_decimals=0, ), CONF_GRID_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_GRID_FREQUENCY: sensor.sensor_schema( unit_of_measurement=UNIT_HERTZ, icon=ICON_CURRENT_AC, accuracy_decimals=1, + device_class=DEVICE_CLASS_FREQUENCY, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_AC_OUTPUT_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_AC_OUTPUT_FREQUENCY: sensor.sensor_schema( unit_of_measurement=UNIT_HERTZ, icon=ICON_CURRENT_AC, accuracy_decimals=1, + device_class=DEVICE_CLASS_FREQUENCY, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_AC_OUTPUT_APPARENT_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS, - accuracy_decimals=1, + accuracy_decimals=0, + device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_AC_OUTPUT_ACTIVE_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_WATT, - accuracy_decimals=1, + accuracy_decimals=0, device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_OUTPUT_LOAD_PERCENT: sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, - accuracy_decimals=1, + icon=ICON_GAUGE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BUS_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, - accuracy_decimals=1, + icon=ICON_FLASH, + accuracy_decimals=0, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BATTERY_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, - accuracy_decimals=1, + icon=ICON_BATTERY, + accuracy_decimals=2, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BATTERY_CHARGING_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, - accuracy_decimals=1, + icon=ICON_CURRENT_DC, + accuracy_decimals=0, device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BATTERY_CAPACITY_PERCENT: sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, - accuracy_decimals=1, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_INVERTER_HEAT_SINK_TEMPERATURE: sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, - accuracy_decimals=1, + accuracy_decimals=0, device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_PV_INPUT_CURRENT_FOR_BATTERY: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, + icon=ICON_SOLAR_PANEL, accuracy_decimals=1, device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_PV_INPUT_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, + icon=ICON_SOLAR_PANEL, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BATTERY_VOLTAGE_SCC: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, - accuracy_decimals=1, + accuracy_decimals=2, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BATTERY_DISCHARGE_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, - accuracy_decimals=1, + icon=ICON_CURRENT_DC, + accuracy_decimals=0, device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BATTERY_VOLTAGE_OFFSET_FOR_FANS_ON: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, @@ -256,12 +298,14 @@ TYPES = { device_class=DEVICE_CLASS_VOLTAGE, ), CONF_EEPROM_VERSION: sensor.sensor_schema( - accuracy_decimals=1, + accuracy_decimals=0, ), CONF_PV_CHARGING_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_WATT, - accuracy_decimals=1, + icon=ICON_SOLAR_POWER, + accuracy_decimals=0, device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), } From 8da8095a6ab196f8dd1c69cafdb80a9a5d4050c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Oct 2025 10:11:13 -0700 Subject: [PATCH 050/394] [tests] Isolate gps component to prevent TinyGPSPlus millis() conflicts (#11499) --- script/analyze_component_buses.py | 1 + 1 file changed, 1 insertion(+) diff --git a/script/analyze_component_buses.py b/script/analyze_component_buses.py index 78f5ca3344..38d1f8c2b7 100755 --- a/script/analyze_component_buses.py +++ b/script/analyze_component_buses.py @@ -77,6 +77,7 @@ ISOLATED_COMPONENTS = { "esphome": "Defines devices/areas in esphome: section that are referenced in other sections - breaks when merged", "ethernet": "Defines ethernet: which conflicts with wifi: used by most components", "ethernet_info": "Related to ethernet component which conflicts with wifi", + "gps": "TinyGPSPlus library declares millis() function that creates ambiguity with ESPHome millis() macro when merged with components using millis() in lambdas", "lvgl": "Defines multiple SDL displays on host platform that conflict when merged with other display configs", "mapping": "Uses dict format for image/display sections incompatible with standard list format - ESPHome merge_config cannot handle", "openthread": "Conflicts with wifi: used by most components", From af321edf8046707ce1059d0d51db64fa34edbd08 Mon Sep 17 00:00:00 2001 From: Links2004 Date: Thu, 23 Oct 2025 17:15:45 +0000 Subject: [PATCH 051/394] [core] handle mixed IP and DNS addresses correctly in resolve_ip_address do not raise error if some addresses are IPs and the mDNS / DNS resolution fails for others fix: #11501 --- esphome/helpers.py | 58 ++++++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/esphome/helpers.py b/esphome/helpers.py index fb7b71775d..8dbbbbce11 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -250,34 +250,42 @@ def resolve_ip_address( # If we have uncached hosts (only non-IP hostnames), resolve them if uncached_hosts: - from esphome.resolver import AsyncResolver + from esphome.core import EsphomeError - resolver = AsyncResolver(uncached_hosts, port) - addr_infos = resolver.resolve() - # Convert aioesphomeapi AddrInfo to our format - for addr_info in addr_infos: - sockaddr = addr_info.sockaddr - if addr_info.family == socket.AF_INET6: - # IPv6 - sockaddr_tuple = ( - sockaddr.address, - sockaddr.port, - sockaddr.flowinfo, - sockaddr.scope_id, + try: + from esphome.resolver import AsyncResolver + + resolver = AsyncResolver(uncached_hosts, port) + addr_infos = resolver.resolve() + # Convert aioesphomeapi AddrInfo to our format + for addr_info in addr_infos: + sockaddr = addr_info.sockaddr + if addr_info.family == socket.AF_INET6: + # IPv6 + sockaddr_tuple = ( + sockaddr.address, + sockaddr.port, + sockaddr.flowinfo, + sockaddr.scope_id, + ) + else: + # IPv4 + sockaddr_tuple = (sockaddr.address, sockaddr.port) + + res.append( + ( + addr_info.family, + addr_info.type, + addr_info.proto, + "", # canonname + sockaddr_tuple, + ) ) + except EsphomeError as err: + if len(res) > 0: + _LOGGER.warning(err) else: - # IPv4 - sockaddr_tuple = (sockaddr.address, sockaddr.port) - - res.append( - ( - addr_info.family, - addr_info.type, - addr_info.proto, - "", # canonname - sockaddr_tuple, - ) - ) + raise err # Sort by preference res.sort(key=addr_preference_) From e490aec6b41e4876964cb3a50249c570942c7fdf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 10:25:36 -0700 Subject: [PATCH 052/394] Bump ruamel-yaml from 0.18.15 to 0.18.16 (#11482) 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 351143591a..4a64bd39cc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ esphome-dashboard==20251013.0 aioesphomeapi==42.3.0 zeroconf==0.148.0 puremagic==1.30 -ruamel.yaml==0.18.15 # dashboard_import +ruamel.yaml==0.18.16 # dashboard_import ruamel.yaml.clib==0.2.14 # dashboard_import esphome-glyphsets==0.2.0 pillow==11.3.0 From 8b67b9f35d0820ddebf8e784c4023038c30d0961 Mon Sep 17 00:00:00 2001 From: Links2004 Date: Thu, 23 Oct 2025 17:54:50 +0000 Subject: [PATCH 053/394] add unit tests for mixed IP and hostname resolution with proper handling of exceptions fix up address handling for mixed IP and hostname resolution --- esphome/helpers.py | 16 +++------------- tests/unit_tests/test_helpers.py | 22 ++++++++++++++++++++-- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/esphome/helpers.py b/esphome/helpers.py index 8dbbbbce11..986026b16d 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -224,30 +224,20 @@ def resolve_ip_address( return res # Process hosts - cached_addresses: list[str] = [] + uncached_hosts: list[str] = [] - has_cache = address_cache is not None for h in hosts: if is_ip_address(h): - if has_cache: - # If we have a cache, treat IPs as cached - cached_addresses.append(h) - else: - # If no cache, pass IPs through to resolver with hostnames - uncached_hosts.append(h) + _add_ip_addresses_to_addrinfo([h], port, res) elif address_cache and (cached := address_cache.get_addresses(h)): - # Found in cache - cached_addresses.extend(cached) + _add_ip_addresses_to_addrinfo(cached, port, res) else: # Not cached, need to resolve if address_cache and address_cache.has_cache(): _LOGGER.info("Host %s not in cache, will need to resolve", h) uncached_hosts.append(h) - # Process cached addresses (includes direct IPs and cached lookups) - _add_ip_addresses_to_addrinfo(cached_addresses, port, res) - # If we have uncached hosts (only non-IP hostnames), resolve them if uncached_hosts: from esphome.core import EsphomeError diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index 87ed901ecb..47b945e0eb 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -454,9 +454,27 @@ def test_resolve_ip_address_mixed_list() -> None: # Mix of IP and hostname - should use async resolver result = helpers.resolve_ip_address(["192.168.1.100", "test.local"], 6053) + assert len(result) == 2 + assert result[0][4][0] == "192.168.1.100" + assert result[1][4][0] == "192.168.1.200" + MockResolver.assert_called_once_with(["test.local"], 6053) + mock_resolver.resolve.assert_called_once() + + +def test_resolve_ip_address_mixed_list_fail() -> None: + """Test resolving a mix of IPs and hostnames with resolve failed.""" + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.side_effect = EsphomeError( + "Error resolving IP address: [test.local]" + ) + + # Mix of IP and hostname - should use async resolver + result = helpers.resolve_ip_address(["192.168.1.100", "test.local"], 6053) + assert len(result) == 1 - assert result[0][4][0] == "192.168.1.200" - MockResolver.assert_called_once_with(["192.168.1.100", "test.local"], 6053) + assert result[0][4][0] == "192.168.1.100" + MockResolver.assert_called_once_with(["test.local"], 6053) mock_resolver.resolve.assert_called_once() From 3e6d1d551d8e7763763bc4be6873d24d8d594e67 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Oct 2025 11:06:09 -0700 Subject: [PATCH 054/394] tweak --- esphome/helpers.py | 59 +++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/esphome/helpers.py b/esphome/helpers.py index 986026b16d..a2c6dd7d49 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -241,41 +241,42 @@ def resolve_ip_address( # If we have uncached hosts (only non-IP hostnames), resolve them if uncached_hosts: from esphome.core import EsphomeError + from esphome.resolver import AsyncResolver + resolver = AsyncResolver(uncached_hosts, port) try: - from esphome.resolver import AsyncResolver - - resolver = AsyncResolver(uncached_hosts, port) addr_infos = resolver.resolve() - # Convert aioesphomeapi AddrInfo to our format - for addr_info in addr_infos: - sockaddr = addr_info.sockaddr - if addr_info.family == socket.AF_INET6: - # IPv6 - sockaddr_tuple = ( - sockaddr.address, - sockaddr.port, - sockaddr.flowinfo, - sockaddr.scope_id, - ) - else: - # IPv4 - sockaddr_tuple = (sockaddr.address, sockaddr.port) - - res.append( - ( - addr_info.family, - addr_info.type, - addr_info.proto, - "", # canonname - sockaddr_tuple, - ) - ) except EsphomeError as err: - if len(res) > 0: + if res: _LOGGER.warning(err) + addr_infos = [] else: - raise err + raise + + # Convert aioesphomeapi AddrInfo to our format + for addr_info in addr_infos: + sockaddr = addr_info.sockaddr + if addr_info.family == socket.AF_INET6: + # IPv6 + sockaddr_tuple = ( + sockaddr.address, + sockaddr.port, + sockaddr.flowinfo, + sockaddr.scope_id, + ) + else: + # IPv4 + sockaddr_tuple = (sockaddr.address, sockaddr.port) + + res.append( + ( + addr_info.family, + addr_info.type, + addr_info.proto, + "", # canonname + sockaddr_tuple, + ) + ) # Sort by preference res.sort(key=addr_preference_) From 267b715bfabf933703970d7e13362ffa7cb9440b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Oct 2025 11:11:45 -0700 Subject: [PATCH 055/394] safer --- esphome/helpers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/helpers.py b/esphome/helpers.py index a2c6dd7d49..775acd0d0c 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -240,16 +240,18 @@ def resolve_ip_address( # If we have uncached hosts (only non-IP hostnames), resolve them if uncached_hosts: + from aioesphomeapi.host_resolver import AddrInfo as AioAddrInfo + from esphome.core import EsphomeError from esphome.resolver import AsyncResolver resolver = AsyncResolver(uncached_hosts, port) + addr_infos: list[AioAddrInfo] = [] try: addr_infos = resolver.resolve() except EsphomeError as err: if res: - _LOGGER.warning(err) - addr_infos = [] + _LOGGER.info("%s (using %d cached IP addresses)", err, len(res)) else: raise From 6dab0b4b497a5e38dc36befb0d4e6fdf302ed107 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Oct 2025 11:12:57 -0700 Subject: [PATCH 056/394] tweaks --- esphome/helpers.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/helpers.py b/esphome/helpers.py index 775acd0d0c..a67f2528d4 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -250,10 +250,9 @@ def resolve_ip_address( try: addr_infos = resolver.resolve() except EsphomeError as err: - if res: - _LOGGER.info("%s (using %d cached IP addresses)", err, len(res)) - else: + if not res: raise + _LOGGER.info("%s (using %d cached IP addresses)", err, len(res)) # Convert aioesphomeapi AddrInfo to our format for addr_info in addr_infos: From c76e44689565a5719ae90d5b638908e45e587eb7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Oct 2025 11:14:24 -0700 Subject: [PATCH 057/394] tweaks --- esphome/helpers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/helpers.py b/esphome/helpers.py index a67f2528d4..ea6abff50a 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -251,8 +251,9 @@ def resolve_ip_address( addr_infos = resolver.resolve() except EsphomeError as err: if not res: + # No pre-resolved addresses available, DNS resolution is fatal raise - _LOGGER.info("%s (using %d cached IP addresses)", err, len(res)) + _LOGGER.info("%s (using %d already resolved IP addresses)", err, len(res)) # Convert aioesphomeapi AddrInfo to our format for addr_info in addr_infos: From fa3ec6f732c3b94d67feba51a820197752074a0b Mon Sep 17 00:00:00 2001 From: Markus <974709+Links2004@users.noreply.github.com> Date: Thu, 23 Oct 2025 20:32:07 +0200 Subject: [PATCH 058/394] [core] handle mixed IP and DNS addresses correctly in resolve_ip_address (#11503) Co-authored-by: J. Nick Koston --- esphome/helpers.py | 29 +++++++++++++++-------------- tests/unit_tests/test_helpers.py | 22 ++++++++++++++++++++-- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/esphome/helpers.py b/esphome/helpers.py index fb7b71775d..ea6abff50a 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -224,36 +224,37 @@ def resolve_ip_address( return res # Process hosts - cached_addresses: list[str] = [] + uncached_hosts: list[str] = [] - has_cache = address_cache is not None for h in hosts: if is_ip_address(h): - if has_cache: - # If we have a cache, treat IPs as cached - cached_addresses.append(h) - else: - # If no cache, pass IPs through to resolver with hostnames - uncached_hosts.append(h) + _add_ip_addresses_to_addrinfo([h], port, res) elif address_cache and (cached := address_cache.get_addresses(h)): - # Found in cache - cached_addresses.extend(cached) + _add_ip_addresses_to_addrinfo(cached, port, res) else: # Not cached, need to resolve if address_cache and address_cache.has_cache(): _LOGGER.info("Host %s not in cache, will need to resolve", h) uncached_hosts.append(h) - # Process cached addresses (includes direct IPs and cached lookups) - _add_ip_addresses_to_addrinfo(cached_addresses, port, res) - # If we have uncached hosts (only non-IP hostnames), resolve them if uncached_hosts: + from aioesphomeapi.host_resolver import AddrInfo as AioAddrInfo + + from esphome.core import EsphomeError from esphome.resolver import AsyncResolver resolver = AsyncResolver(uncached_hosts, port) - addr_infos = resolver.resolve() + addr_infos: list[AioAddrInfo] = [] + try: + addr_infos = resolver.resolve() + except EsphomeError as err: + if not res: + # No pre-resolved addresses available, DNS resolution is fatal + raise + _LOGGER.info("%s (using %d already resolved IP addresses)", err, len(res)) + # Convert aioesphomeapi AddrInfo to our format for addr_info in addr_infos: sockaddr = addr_info.sockaddr diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index 87ed901ecb..47b945e0eb 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -454,9 +454,27 @@ def test_resolve_ip_address_mixed_list() -> None: # Mix of IP and hostname - should use async resolver result = helpers.resolve_ip_address(["192.168.1.100", "test.local"], 6053) + assert len(result) == 2 + assert result[0][4][0] == "192.168.1.100" + assert result[1][4][0] == "192.168.1.200" + MockResolver.assert_called_once_with(["test.local"], 6053) + mock_resolver.resolve.assert_called_once() + + +def test_resolve_ip_address_mixed_list_fail() -> None: + """Test resolving a mix of IPs and hostnames with resolve failed.""" + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.side_effect = EsphomeError( + "Error resolving IP address: [test.local]" + ) + + # Mix of IP and hostname - should use async resolver + result = helpers.resolve_ip_address(["192.168.1.100", "test.local"], 6053) + assert len(result) == 1 - assert result[0][4][0] == "192.168.1.200" - MockResolver.assert_called_once_with(["192.168.1.100", "test.local"], 6053) + assert result[0][4][0] == "192.168.1.100" + MockResolver.assert_called_once_with(["test.local"], 6053) mock_resolver.resolve.assert_called_once() From de2f475dbd405506b583ef02e42509f6fff25a0a Mon Sep 17 00:00:00 2001 From: Peter Zich Date: Mon, 20 Oct 2025 06:13:13 -0700 Subject: [PATCH 059/394] [hdc1080] Make HDC1080_CMD_CONFIGURATION failure a warning (and log it) (#11355) Co-authored-by: J. Nick Koston --- esphome/components/hdc1080/hdc1080.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/hdc1080/hdc1080.cpp b/esphome/components/hdc1080/hdc1080.cpp index 71b7cd7e6e..fa293f6fc5 100644 --- a/esphome/components/hdc1080/hdc1080.cpp +++ b/esphome/components/hdc1080/hdc1080.cpp @@ -16,7 +16,8 @@ void HDC1080Component::setup() { // if configuration fails - there is a problem if (this->write_register(HDC1080_CMD_CONFIGURATION, config, 2) != i2c::ERROR_OK) { - this->mark_failed(); + ESP_LOGW(TAG, "Failed to configure HDC1080"); + this->status_set_warning(); return; } } From febee437d67ee0e4af4ebac8af8a117269d0b0d5 Mon Sep 17 00:00:00 2001 From: Anton Sergunov Date: Wed, 22 Oct 2025 03:10:25 +0600 Subject: [PATCH 060/394] [uart] Make rx pin respect pullup and pulldown settings (#9248) --- esphome/components/uart/uart_component_esp8266.cpp | 7 +++++++ esphome/components/uart/uart_component_esp_idf.cpp | 10 ++++++++++ esphome/components/uart/uart_component_libretiny.cpp | 7 +++++++ esphome/components/uart/uart_component_rp2040.cpp | 7 +++++++ 4 files changed, 31 insertions(+) diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index b2bf2bacf1..7a453dbb50 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -56,6 +56,13 @@ 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(); + } + // Use Arduino HardwareSerial UARTs if all used pins match the ones // preconfigured by the platform. For example if RX disabled but TX pin // is 1 we still want to use Serial. diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 7530856b1e..cffa3308eb 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -6,6 +6,9 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/gpio.h" +#include "driver/gpio.h" +#include "soc/gpio_num.h" #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" @@ -104,6 +107,13 @@ 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(); + } + int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index 8a7a301cfe..9c065fe5df 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -46,6 +46,13 @@ uint16_t LibreTinyUARTComponent::get_config() { } void LibreTinyUARTComponent::setup() { + if (this->rx_pin_) { + this->rx_pin_->setup(); + } + if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { + this->tx_pin_->setup(); + } + int8_t tx_pin = tx_pin_ == nullptr ? -1 : tx_pin_->get_pin(); int8_t rx_pin = rx_pin_ == nullptr ? -1 : rx_pin_->get_pin(); bool tx_inverted = tx_pin_ != nullptr && tx_pin_->is_inverted(); diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2040.cpp index ae3042fb77..c78691653d 100644 --- a/esphome/components/uart/uart_component_rp2040.cpp +++ b/esphome/components/uart/uart_component_rp2040.cpp @@ -52,6 +52,13 @@ 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(); + } + uint16_t config = get_config(); constexpr uint32_t valid_tx_uart_0 = __bitset({0, 12, 16, 28}); From daeb8ef88cba3607426890f537d4fb0fae9441ae Mon Sep 17 00:00:00 2001 From: Markus <974709+Links2004@users.noreply.github.com> Date: Thu, 23 Oct 2025 20:32:07 +0200 Subject: [PATCH 061/394] [core] handle mixed IP and DNS addresses correctly in resolve_ip_address (#11503) Co-authored-by: J. Nick Koston --- esphome/helpers.py | 29 +++++++++++++++-------------- tests/unit_tests/test_helpers.py | 22 ++++++++++++++++++++-- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/esphome/helpers.py b/esphome/helpers.py index fb7b71775d..ea6abff50a 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -224,36 +224,37 @@ def resolve_ip_address( return res # Process hosts - cached_addresses: list[str] = [] + uncached_hosts: list[str] = [] - has_cache = address_cache is not None for h in hosts: if is_ip_address(h): - if has_cache: - # If we have a cache, treat IPs as cached - cached_addresses.append(h) - else: - # If no cache, pass IPs through to resolver with hostnames - uncached_hosts.append(h) + _add_ip_addresses_to_addrinfo([h], port, res) elif address_cache and (cached := address_cache.get_addresses(h)): - # Found in cache - cached_addresses.extend(cached) + _add_ip_addresses_to_addrinfo(cached, port, res) else: # Not cached, need to resolve if address_cache and address_cache.has_cache(): _LOGGER.info("Host %s not in cache, will need to resolve", h) uncached_hosts.append(h) - # Process cached addresses (includes direct IPs and cached lookups) - _add_ip_addresses_to_addrinfo(cached_addresses, port, res) - # If we have uncached hosts (only non-IP hostnames), resolve them if uncached_hosts: + from aioesphomeapi.host_resolver import AddrInfo as AioAddrInfo + + from esphome.core import EsphomeError from esphome.resolver import AsyncResolver resolver = AsyncResolver(uncached_hosts, port) - addr_infos = resolver.resolve() + addr_infos: list[AioAddrInfo] = [] + try: + addr_infos = resolver.resolve() + except EsphomeError as err: + if not res: + # No pre-resolved addresses available, DNS resolution is fatal + raise + _LOGGER.info("%s (using %d already resolved IP addresses)", err, len(res)) + # Convert aioesphomeapi AddrInfo to our format for addr_info in addr_infos: sockaddr = addr_info.sockaddr diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index 87ed901ecb..47b945e0eb 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -454,9 +454,27 @@ def test_resolve_ip_address_mixed_list() -> None: # Mix of IP and hostname - should use async resolver result = helpers.resolve_ip_address(["192.168.1.100", "test.local"], 6053) + assert len(result) == 2 + assert result[0][4][0] == "192.168.1.100" + assert result[1][4][0] == "192.168.1.200" + MockResolver.assert_called_once_with(["test.local"], 6053) + mock_resolver.resolve.assert_called_once() + + +def test_resolve_ip_address_mixed_list_fail() -> None: + """Test resolving a mix of IPs and hostnames with resolve failed.""" + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.side_effect = EsphomeError( + "Error resolving IP address: [test.local]" + ) + + # Mix of IP and hostname - should use async resolver + result = helpers.resolve_ip_address(["192.168.1.100", "test.local"], 6053) + assert len(result) == 1 - assert result[0][4][0] == "192.168.1.200" - MockResolver.assert_called_once_with(["192.168.1.100", "test.local"], 6053) + assert result[0][4][0] == "192.168.1.100" + MockResolver.assert_called_once_with(["test.local"], 6053) mock_resolver.resolve.assert_called_once() From a32a1d11fb2024e2d7d917b3a1b4280edddb5a44 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 24 Oct 2025 07:51:38 +1300 Subject: [PATCH 062/394] Bump version to 2025.10.3 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 6ed69336d2..f770defaf2 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.10.2 +PROJECT_NUMBER = 2025.10.3 # 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 db8903fd96..712dc85221 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.10.2" +__version__ = "2025.10.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 2440bbdceb385319821876d79f8a26d9646bdd85 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Oct 2025 20:01:23 -0700 Subject: [PATCH 063/394] [core][sensor] Eliminate redundant default value setters in generated code (#11495) --- esphome/components/sensor/__init__.py | 4 +++- esphome/core/entity_helpers.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 7e91bb83c4..93283e4d47 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -878,7 +878,9 @@ async def setup_sensor_core_(var, config): cg.add(var.set_unit_of_measurement(unit_of_measurement)) if (accuracy_decimals := config.get(CONF_ACCURACY_DECIMALS)) is not None: cg.add(var.set_accuracy_decimals(accuracy_decimals)) - cg.add(var.set_force_update(config[CONF_FORCE_UPDATE])) + # Only set force_update if True (default is False) + if config[CONF_FORCE_UPDATE]: + cg.add(var.set_force_update(True)) if config.get(CONF_FILTERS): # must exist and not be empty filters = await build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index f0a04b4860..9b4786f835 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -105,7 +105,9 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None: config[CONF_NAME], platform, ) - add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) + # Only set disabled_by_default if True (default is False) + if config[CONF_DISABLED_BY_DEFAULT]: + add(var.set_disabled_by_default(True)) if CONF_INTERNAL in config: add(var.set_internal(config[CONF_INTERNAL])) if CONF_ICON in config: From 2c85ba037ec96974b077fe471f94d96e623209d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Oct 2025 20:01:48 -0700 Subject: [PATCH 064/394] [http_request] Pass collect_headers by const reference instead of by value (#11494) --- esphome/components/http_request/http_request.h | 2 +- esphome/components/http_request/http_request_arduino.cpp | 2 +- esphome/components/http_request/http_request_arduino.h | 2 +- esphome/components/http_request/http_request_host.cpp | 2 +- esphome/components/http_request/http_request_host.h | 2 +- esphome/components/http_request/http_request_idf.cpp | 2 +- esphome/components/http_request/http_request_idf.h | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index bb14cc6f51..40c85d51ed 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -169,7 +169,7 @@ class HttpRequestComponent : public Component { protected: virtual std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - std::set collect_headers) = 0; + const std::set &collect_headers) = 0; const char *useragent_{nullptr}; bool follow_redirects_{}; uint16_t redirect_limit_{}; diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index dfdbbd3fab..c64a7be554 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -17,7 +17,7 @@ static const char *const TAG = "http_request.arduino"; std::shared_ptr HttpRequestArduino::perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - std::set collect_headers) { + const std::set &collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index c8208c74d8..b736bb56d1 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -33,7 +33,7 @@ class HttpRequestArduino : public HttpRequestComponent { protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - std::set collect_headers) override; + const std::set &collect_headers) override; }; } // namespace http_request diff --git a/esphome/components/http_request/http_request_host.cpp b/esphome/components/http_request/http_request_host.cpp index c20ea552b7..402affc1d1 100644 --- a/esphome/components/http_request/http_request_host.cpp +++ b/esphome/components/http_request/http_request_host.cpp @@ -20,7 +20,7 @@ static const char *const TAG = "http_request.host"; std::shared_ptr HttpRequestHost::perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - std::set response_headers) { + const std::set &response_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); diff --git a/esphome/components/http_request/http_request_host.h b/esphome/components/http_request/http_request_host.h index fdd72e7ea5..886ba94938 100644 --- a/esphome/components/http_request/http_request_host.h +++ b/esphome/components/http_request/http_request_host.h @@ -20,7 +20,7 @@ class HttpRequestHost : public HttpRequestComponent { public: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - std::set response_headers) override; + const std::set &response_headers) override; void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; } protected: diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index a91c0bfc25..34a3fb87eb 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -55,7 +55,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { std::shared_ptr HttpRequestIDF::perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - std::set collect_headers) { + const std::set &collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); ESP_LOGE(TAG, "HTTP Request failed; Not connected to network"); diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index 90dee0be68..e51b3aaebc 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -39,7 +39,7 @@ class HttpRequestIDF : public HttpRequestComponent { protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - std::set collect_headers) override; + const std::set &collect_headers) override; // if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE uint16_t buffer_size_rx_{}; uint16_t buffer_size_tx_{}; From 5426f8736b5c6313e69ea3c5b6e34f960a08163a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Oct 2025 22:58:09 -0700 Subject: [PATCH 065/394] [esphome][ota] Add write_byte_() helper to reduce code duplication --- .../components/esphome/ota/ota_esphome.cpp | 28 ++++++------------- esphome/components/esphome/ota/ota_esphome.h | 1 + 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 569268ea15..b85d660272 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -281,19 +281,15 @@ void ESPHomeOTAComponent::handle_data_() { #endif // Acknowledge auth OK - 1 byte - buf[0] = ota::OTA_RESPONSE_AUTH_OK; - this->writeall_(buf, 1); + this->write_byte_(ota::OTA_RESPONSE_AUTH_OK); // Read size, 4 bytes MSB first if (!this->readall_(buf, 4)) { this->log_read_error_(LOG_STR("size")); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } - ota_size = 0; - for (uint8_t i = 0; i < 4; i++) { - ota_size <<= 8; - ota_size |= buf[i]; - } + ota_size = (static_cast(buf[0]) << 24) | (static_cast(buf[1]) << 16) | + (static_cast(buf[2]) << 8) | buf[3]; ESP_LOGV(TAG, "Size is %u bytes", ota_size); // Now that we've passed authentication and are actually @@ -313,8 +309,7 @@ void ESPHomeOTAComponent::handle_data_() { update_started = true; // Acknowledge prepare OK - 1 byte - buf[0] = ota::OTA_RESPONSE_UPDATE_PREPARE_OK; - this->writeall_(buf, 1); + this->write_byte_(ota::OTA_RESPONSE_UPDATE_PREPARE_OK); // Read binary MD5, 32 bytes if (!this->readall_(buf, 32)) { @@ -326,8 +321,7 @@ void ESPHomeOTAComponent::handle_data_() { this->backend_->set_update_md5(sbuf); // Acknowledge MD5 OK - 1 byte - buf[0] = ota::OTA_RESPONSE_BIN_MD5_OK; - this->writeall_(buf, 1); + this->write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK); while (total < ota_size) { // TODO: timeout check @@ -354,8 +348,7 @@ void ESPHomeOTAComponent::handle_data_() { total += read; #if USE_OTA_VERSION == 2 while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) { - buf[0] = ota::OTA_RESPONSE_CHUNK_OK; - this->writeall_(buf, 1); + this->write_byte_(ota::OTA_RESPONSE_CHUNK_OK); size_acknowledged += OTA_BLOCK_SIZE; } #endif @@ -374,8 +367,7 @@ void ESPHomeOTAComponent::handle_data_() { } // Acknowledge receive OK - 1 byte - buf[0] = ota::OTA_RESPONSE_RECEIVE_OK; - this->writeall_(buf, 1); + this->write_byte_(ota::OTA_RESPONSE_RECEIVE_OK); error_code = this->backend_->end(); if (error_code != ota::OTA_RESPONSE_OK) { @@ -384,8 +376,7 @@ void ESPHomeOTAComponent::handle_data_() { } // Acknowledge Update end OK - 1 byte - buf[0] = ota::OTA_RESPONSE_UPDATE_END_OK; - this->writeall_(buf, 1); + this->write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK); // Read ACK if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) { @@ -404,8 +395,7 @@ void ESPHomeOTAComponent::handle_data_() { App.safe_reboot(); error: - buf[0] = static_cast(error_code); - this->writeall_(buf, 1); + this->write_byte_(static_cast(error_code)); this->cleanup_connection_(); if (this->backend_ != nullptr && update_started) { diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index d4a8410d35..057461e6a4 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -53,6 +53,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent { #endif // USE_OTA_PASSWORD bool readall_(uint8_t *buf, size_t len); bool writeall_(const uint8_t *buf, size_t len); + inline bool write_byte_(uint8_t byte) { return this->writeall_(&byte, 1); } bool try_read_(size_t to_read, const LogString *desc); bool try_write_(size_t to_write, const LogString *desc); From d27e78e909be543bfc6e779ff988de6526686d9d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 04:13:34 -0700 Subject: [PATCH 066/394] [select] Store options in flash to reduce RAM usage --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_pb2.cpp | 8 +++---- esphome/components/api/api_pb2.h | 2 +- .../components/copy/select/copy_select.cpp | 3 ++- esphome/components/lvgl/lvgl_esphome.h | 2 +- esphome/components/lvgl/select/lvgl_select.h | 12 +++++++++- esphome/components/select/select.cpp | 5 ++-- esphome/components/select/select_traits.cpp | 4 ++-- esphome/components/select/select_traits.h | 10 ++++---- .../template/select/template_select.cpp | 2 +- esphome/core/helpers.h | 10 ++++++++ script/api_protobuf/api_protobuf.py | 24 +++++++++++++------ 12 files changed, 58 insertions(+), 26 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index d202486cfa..b12b53fd00 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1143,7 +1143,7 @@ message ListEntitiesSelectResponse { reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; - repeated string options = 6 [(container_pointer) = "std::vector"]; + repeated string options = 6 [(container_pointer_no_template) = "FixedVector"]; bool disabled_by_default = 7; EntityCategory entity_category = 8; uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 37bcf5d8a0..3472707d3c 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1475,8 +1475,8 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon_ref_); #endif - for (const auto &it : *this->options) { - buffer.encode_string(6, it, true); + for (const char *it : *this->options) { + buffer.encode_string(6, it, strlen(it), true); } buffer.encode_bool(7, this->disabled_by_default); buffer.encode_uint32(8, static_cast(this->entity_category)); @@ -1492,8 +1492,8 @@ void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const { size.add_length(1, this->icon_ref_.size()); #endif if (!this->options->empty()) { - for (const auto &it : *this->options) { - size.add_length_force(1, it.size()); + for (const char *it : *this->options) { + size.add_length_force(1, strlen(it)); } } size.add_bool(1, this->disabled_by_default); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index ed49498176..2f23201dcd 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1534,7 +1534,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_select_response"; } #endif - const std::vector *options{}; + const FixedVector *options{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/copy/select/copy_select.cpp b/esphome/components/copy/select/copy_select.cpp index bdcbd0b42c..6618ae6347 100644 --- a/esphome/components/copy/select/copy_select.cpp +++ b/esphome/components/copy/select/copy_select.cpp @@ -9,7 +9,8 @@ static const char *const TAG = "copy.select"; void CopySelect::setup() { source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); }); - traits.set_options(source_->traits.get_options()); + const auto &source_options = source_->traits.get_options(); + traits.set_options({source_options.begin(), source_options.end()}); if (source_->has_state()) this->publish_state(source_->state); diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 3ae67e8a0b..d3dc8fac5a 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -358,7 +358,7 @@ class LvSelectable : public LvCompound { virtual void set_selected_index(size_t index, lv_anim_enable_t anim) = 0; void set_selected_text(const std::string &text, lv_anim_enable_t anim); std::string get_selected_text(); - std::vector get_options() { return this->options_; } + const std::vector &get_options() { return this->options_; } void set_options(std::vector options); protected: diff --git a/esphome/components/lvgl/select/lvgl_select.h b/esphome/components/lvgl/select/lvgl_select.h index a0e60295a6..0ab28d372d 100644 --- a/esphome/components/lvgl/select/lvgl_select.h +++ b/esphome/components/lvgl/select/lvgl_select.h @@ -53,7 +53,17 @@ class LVGLSelect : public select::Select, public Component { this->widget_->set_selected_text(value, this->anim_); this->publish(); } - void set_options_() { this->traits.set_options(this->widget_->get_options()); } + void set_options_() { + // Widget uses std::vector, SelectTraits uses FixedVector + // Convert by extracting c_str() pointers + const auto &opts = this->widget_->get_options(); + std::vector opt_ptrs; + opt_ptrs.reserve(opts.size()); + for (const auto &opt : opts) { + opt_ptrs.push_back(opt.c_str()); + } + this->traits.set_options({opt_ptrs.begin(), opt_ptrs.end()}); + } LvSelectable *widget_; lv_anim_enable_t anim_; diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 16e8288ca1..66cd51e15a 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -1,5 +1,6 @@ #include "select.h" #include "esphome/core/log.h" +#include namespace esphome { namespace select { @@ -35,7 +36,7 @@ size_t Select::size() const { optional Select::index_of(const std::string &option) const { const auto &options = traits.get_options(); for (size_t i = 0; i < options.size(); i++) { - if (options[i] == option) { + if (strcmp(options[i], option.c_str()) == 0) { return i; } } @@ -53,7 +54,7 @@ optional Select::active_index() const { optional Select::at(size_t index) const { if (this->has_index(index)) { const auto &options = traits.get_options(); - return options.at(index); + return std::string(options.at(index)); } else { return {}; } diff --git a/esphome/components/select/select_traits.cpp b/esphome/components/select/select_traits.cpp index a8cd4290c8..06bd2404c2 100644 --- a/esphome/components/select/select_traits.cpp +++ b/esphome/components/select/select_traits.cpp @@ -3,9 +3,9 @@ namespace esphome { namespace select { -void SelectTraits::set_options(std::vector options) { this->options_ = std::move(options); } +void SelectTraits::set_options(std::initializer_list options) { this->options_ = options; } -const std::vector &SelectTraits::get_options() const { return this->options_; } +const FixedVector &SelectTraits::get_options() const { return this->options_; } } // namespace select } // namespace esphome diff --git a/esphome/components/select/select_traits.h b/esphome/components/select/select_traits.h index 128066dd6b..8f8fe3b71f 100644 --- a/esphome/components/select/select_traits.h +++ b/esphome/components/select/select_traits.h @@ -1,18 +1,18 @@ #pragma once -#include -#include +#include "esphome/core/helpers.h" +#include namespace esphome { namespace select { class SelectTraits { public: - void set_options(std::vector options); - const std::vector &get_options() const; + void set_options(std::initializer_list options); + const FixedVector &get_options() const; protected: - std::vector options_; + FixedVector options_; }; } // namespace select diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 95b0ee0d2b..7f7aa2c43f 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -22,7 +22,7 @@ void TemplateSelect::setup() { ESP_LOGD(TAG, "State from initial (could not load stored index): %s", value.c_str()); } else if (!this->has_index(index)) { value = this->initial_option_; - ESP_LOGD(TAG, "State from initial (restored index %d out of bounds): %s", index, value.c_str()); + ESP_LOGD(TAG, "State from initial (restored index %zu out of bounds): %s", index, value.c_str()); } else { value = this->at(index).value(); ESP_LOGD(TAG, "State from restore: %s", value.c_str()); diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 9b0591c9c5..7b4f2ad21f 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -304,6 +304,11 @@ template class FixedVector { return data_[size_ - 1]; } + /// Access first element (no bounds checking - matches std::vector behavior) + /// Caller must ensure vector is not empty (size() > 0) + T &front() { return data_[0]; } + const T &front() const { return data_[0]; } + /// Access last element (no bounds checking - matches std::vector behavior) /// Caller must ensure vector is not empty (size() > 0) T &back() { return data_[size_ - 1]; } @@ -317,6 +322,11 @@ template class FixedVector { T &operator[](size_t i) { return data_[i]; } const T &operator[](size_t i) const { return data_[i]; } + /// Access element with bounds checking (matches std::vector behavior) + /// Caller must ensure index is valid (i < size()) + T &at(size_t i) { return data_[i]; } + const T &at(size_t i) const { return data_[i]; } + // Iterator support for range-based for loops T *begin() { return data_; } T *end() { return data_ + size_; } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 2f83b0bd79..f5cca0e0de 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1533,11 +1533,16 @@ class RepeatedTypeInfo(TypeInfo): def encode_content(self) -> str: if self._use_pointer: # For pointer fields, just dereference (pointer should never be null in our use case) - o = f"for (const auto &it : *this->{self.field_name}) {{\n" - if isinstance(self._ti, EnumType): - o += f" buffer.{self._ti.encode_func}({self.number}, static_cast(it), true);\n" + # Special handling for const char* elements (when container_no_template contains "const char") + if "const char" in self._container_no_template: + o = f"for (const char *it : *this->{self.field_name}) {{\n" + o += f" buffer.{self._ti.encode_func}({self.number}, it, strlen(it), true);\n" else: - o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n" + o = f"for (const auto &it : *this->{self.field_name}) {{\n" + if isinstance(self._ti, EnumType): + o += f" buffer.{self._ti.encode_func}({self.number}, static_cast(it), true);\n" + else: + o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n" o += "}" return o o = f"for (auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{\n" @@ -1588,9 +1593,14 @@ class RepeatedTypeInfo(TypeInfo): o += f" size.add_precalculated_size({size_expr} * {bytes_per_element});\n" else: # Other types need the actual value - auto_ref = "" if self._ti_is_bool else "&" - o += f" for (const auto {auto_ref}it : {container_ref}) {{\n" - o += f" {self._ti.get_size_calculation('it', True)}\n" + # Special handling for const char* elements + if self._use_pointer and "const char" in self._container_no_template: + o += f" for (const char *it : {container_ref}) {{\n" + o += " size.add_length_force(1, strlen(it));\n" + else: + auto_ref = "" if self._ti_is_bool else "&" + o += f" for (const auto {auto_ref}it : {container_ref}) {{\n" + o += f" {self._ti.get_size_calculation('it', True)}\n" o += " }\n" o += "}" From 3d6224d1b10d4431bb0dacfe1fd74c9ab0fb71aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 04:22:22 -0700 Subject: [PATCH 067/394] [select] Store options in flash to reduce RAM usage --- esphome/components/copy/select/copy_select.cpp | 2 +- esphome/components/select/select_traits.cpp | 2 ++ esphome/components/select/select_traits.h | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/copy/select/copy_select.cpp b/esphome/components/copy/select/copy_select.cpp index 6618ae6347..be90af5a13 100644 --- a/esphome/components/copy/select/copy_select.cpp +++ b/esphome/components/copy/select/copy_select.cpp @@ -10,7 +10,7 @@ void CopySelect::setup() { source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); }); const auto &source_options = source_->traits.get_options(); - traits.set_options({source_options.begin(), source_options.end()}); + traits.set_options(source_options); if (source_->has_state()) this->publish_state(source_->state); diff --git a/esphome/components/select/select_traits.cpp b/esphome/components/select/select_traits.cpp index 06bd2404c2..90a70393d1 100644 --- a/esphome/components/select/select_traits.cpp +++ b/esphome/components/select/select_traits.cpp @@ -5,6 +5,8 @@ namespace select { void SelectTraits::set_options(std::initializer_list options) { this->options_ = options; } +void SelectTraits::set_options(const FixedVector &options) { this->options_ = options; } + const FixedVector &SelectTraits::get_options() const { return this->options_; } } // namespace select diff --git a/esphome/components/select/select_traits.h b/esphome/components/select/select_traits.h index 8f8fe3b71f..b504f08298 100644 --- a/esphome/components/select/select_traits.h +++ b/esphome/components/select/select_traits.h @@ -9,6 +9,7 @@ namespace select { class SelectTraits { public: void set_options(std::initializer_list options); + void set_options(const FixedVector &options); const FixedVector &get_options() const; protected: From 18b12f845dd15ed0dc1b8332765d0b81c1e3b46d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 04:22:52 -0700 Subject: [PATCH 068/394] [select] Store options in flash to reduce RAM usage --- esphome/components/lvgl/select/lvgl_select.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/lvgl/select/lvgl_select.h b/esphome/components/lvgl/select/lvgl_select.h index 0ab28d372d..3b1fd67d68 100644 --- a/esphome/components/lvgl/select/lvgl_select.h +++ b/esphome/components/lvgl/select/lvgl_select.h @@ -57,12 +57,12 @@ class LVGLSelect : public select::Select, public Component { // Widget uses std::vector, SelectTraits uses FixedVector // Convert by extracting c_str() pointers const auto &opts = this->widget_->get_options(); - std::vector opt_ptrs; - opt_ptrs.reserve(opts.size()); - for (const auto &opt : opts) { - opt_ptrs.push_back(opt.c_str()); + FixedVector opt_ptrs; + opt_ptrs.init(opts.size()); + for (size_t i = 0; i < opts.size(); i++) { + opt_ptrs[i] = opts[i].c_str(); } - this->traits.set_options({opt_ptrs.begin(), opt_ptrs.end()}); + this->traits.set_options(opt_ptrs); } LvSelectable *widget_; From 83e4013a259bfac890bba56bce403b42edd661a1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 04:27:41 -0700 Subject: [PATCH 069/394] [select] Store options in flash to reduce RAM usage --- esphome/components/copy/select/copy_select.cpp | 3 +-- esphome/components/select/select.cpp | 2 +- esphome/core/helpers.h | 5 ----- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/esphome/components/copy/select/copy_select.cpp b/esphome/components/copy/select/copy_select.cpp index be90af5a13..bdcbd0b42c 100644 --- a/esphome/components/copy/select/copy_select.cpp +++ b/esphome/components/copy/select/copy_select.cpp @@ -9,8 +9,7 @@ static const char *const TAG = "copy.select"; void CopySelect::setup() { source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); }); - const auto &source_options = source_->traits.get_options(); - traits.set_options(source_options); + traits.set_options(source_->traits.get_options()); if (source_->has_state()) this->publish_state(source_->state); diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 66cd51e15a..5961d71faa 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -54,7 +54,7 @@ optional Select::active_index() const { optional Select::at(size_t index) const { if (this->has_index(index)) { const auto &options = traits.get_options(); - return std::string(options.at(index)); + return std::string(options[index]); } else { return {}; } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 7b4f2ad21f..15f05b9b6f 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -322,11 +322,6 @@ template class FixedVector { T &operator[](size_t i) { return data_[i]; } const T &operator[](size_t i) const { return data_[i]; } - /// Access element with bounds checking (matches std::vector behavior) - /// Caller must ensure index is valid (i < size()) - T &at(size_t i) { return data_[i]; } - const T &at(size_t i) const { return data_[i]; } - // Iterator support for range-based for loops T *begin() { return data_; } T *end() { return data_ + size_; } From 09f97d86e68761ed8c79decd65f960b6b0ab055d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 04:31:16 -0700 Subject: [PATCH 070/394] [select] Store options in flash to reduce RAM usage --- esphome/components/select/select_traits.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/select/select_traits.cpp b/esphome/components/select/select_traits.cpp index 90a70393d1..dc849b8b7e 100644 --- a/esphome/components/select/select_traits.cpp +++ b/esphome/components/select/select_traits.cpp @@ -5,7 +5,12 @@ namespace select { void SelectTraits::set_options(std::initializer_list options) { this->options_ = options; } -void SelectTraits::set_options(const FixedVector &options) { this->options_ = options; } +void SelectTraits::set_options(const FixedVector &options) { + this->options_.init(options.size()); + for (size_t i = 0; i < options.size(); i++) { + this->options_[i] = options[i]; + } +} const FixedVector &SelectTraits::get_options() const { return this->options_; } From 3ae82f6b98014d9aa3409693660deddc2b2a484b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 04:39:55 -0700 Subject: [PATCH 071/394] [select] Store options in flash to reduce RAM usage --- esphome/components/api/api_pb2_dump.cpp | 6 ++++++ script/api_protobuf/api_protobuf.py | 27 ++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index e803125f53..d94ceaaa9c 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -88,6 +88,12 @@ static void dump_field(std::string &out, const char *field_name, StringRef value out.append("\n"); } +static void dump_field(std::string &out, const char *field_name, const char *value, int indent = 2) { + append_field_prefix(out, field_name, indent); + out.append("'").append(value).append("'"); + out.append("\n"); +} + template static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) { append_field_prefix(out, field_name, indent); out.append(proto_enum_to_string(value)); diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index f5cca0e0de..394e92b9a7 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1162,7 +1162,11 @@ class SInt64Type(TypeInfo): def _generate_array_dump_content( - ti, field_name: str, name: str, is_bool: bool = False + ti, + field_name: str, + name: str, + is_bool: bool = False, + is_const_char_ptr: bool = False, ) -> str: """Generate dump content for array types (repeated or fixed array). @@ -1170,7 +1174,10 @@ def _generate_array_dump_content( """ o = f"for (const auto {'' if is_bool else '&'}it : {field_name}) {{\n" # Check if underlying type can use dump_field - if ti.can_use_dump_field(): + if is_const_char_ptr: + # Special case for const char* - use it directly + o += f' dump_field(out, "{name}", it, 4);\n' + elif ti.can_use_dump_field(): # For types that have dump_field overloads, use them with extra indent # std::vector iterators return proxy objects, need explicit cast value_expr = "static_cast(it)" if is_bool else ti.dump_field_value("it") @@ -1555,10 +1562,18 @@ class RepeatedTypeInfo(TypeInfo): @property def dump_content(self) -> str: + # Check if this is const char* elements + is_const_char_ptr = ( + self._use_pointer and "const char" in self._container_no_template + ) if self._use_pointer: # For pointer fields, dereference and use the existing helper return _generate_array_dump_content( - self._ti, f"*this->{self.field_name}", self.name, is_bool=False + self._ti, + f"*this->{self.field_name}", + self.name, + is_bool=False, + is_const_char_ptr=is_const_char_ptr, ) return _generate_array_dump_content( self._ti, f"this->{self.field_name}", self.name, is_bool=self._ti_is_bool @@ -2552,6 +2567,12 @@ static void dump_field(std::string &out, const char *field_name, StringRef value out.append("\\n"); } +static void dump_field(std::string &out, const char *field_name, const char *value, int indent = 2) { + append_field_prefix(out, field_name, indent); + out.append("'").append(value).append("'"); + out.append("\\n"); +} + template static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) { append_field_prefix(out, field_name, indent); From 4135e0b5db28c7585594531e6d4b95dc11bec659 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 06:43:03 -0700 Subject: [PATCH 072/394] fixes --- .../modbus_controller/select/modbus_select.cpp | 10 ++++++---- esphome/components/tuya/select/tuya_select.cpp | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/modbus_controller/select/modbus_select.cpp b/esphome/components/modbus_controller/select/modbus_select.cpp index 56b8c783ed..674dd05e55 100644 --- a/esphome/components/modbus_controller/select/modbus_select.cpp +++ b/esphome/components/modbus_controller/select/modbus_select.cpp @@ -41,10 +41,12 @@ void ModbusSelect::parse_and_publish(const std::vector &data) { } void ModbusSelect::control(const std::string &value) { - auto options = this->traits.get_options(); - auto opt_it = std::find(options.cbegin(), options.cend(), value); - size_t idx = std::distance(options.cbegin(), opt_it); - optional mapval = this->mapping_[idx]; + auto idx = this->index_of(value); + if (!idx.has_value()) { + ESP_LOGW(TAG, "Invalid option '%s'", value.c_str()); + return; + } + optional mapval = this->mapping_[idx.value()]; ESP_LOGD(TAG, "Found value %lld for option '%s'", *mapval, value.c_str()); std::vector data; diff --git a/esphome/components/tuya/select/tuya_select.cpp b/esphome/components/tuya/select/tuya_select.cpp index 91ddbc77ec..7b175ee195 100644 --- a/esphome/components/tuya/select/tuya_select.cpp +++ b/esphome/components/tuya/select/tuya_select.cpp @@ -10,7 +10,7 @@ void TuyaSelect::setup() { this->parent_->register_listener(this->select_id_, [this](const TuyaDatapoint &datapoint) { uint8_t enum_value = datapoint.value_enum; ESP_LOGV(TAG, "MCU reported select %u value %u", this->select_id_, enum_value); - auto options = this->traits.get_options(); + const auto &options = this->traits.get_options(); auto mappings = this->mappings_; auto it = std::find(mappings.cbegin(), mappings.cend(), enum_value); if (it == mappings.end()) { @@ -49,9 +49,9 @@ void TuyaSelect::dump_config() { " Data type: %s\n" " Options are:", this->select_id_, this->is_int_ ? "int" : "enum"); - auto options = this->traits.get_options(); + const auto &options = this->traits.get_options(); for (size_t i = 0; i < this->mappings_.size(); i++) { - ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options.at(i).c_str()); + ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options[i]); } } From b2cded14ecfcf995df17855badf4a7fd9c33c92f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 06:46:54 -0700 Subject: [PATCH 073/394] tweak --- esphome/components/select/select.cpp | 2 +- esphome/components/tuya/select/tuya_select.cpp | 2 +- esphome/core/helpers.h | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 5961d71faa..66cd51e15a 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -54,7 +54,7 @@ optional Select::active_index() const { optional Select::at(size_t index) const { if (this->has_index(index)) { const auto &options = traits.get_options(); - return std::string(options[index]); + return std::string(options.at(index)); } else { return {}; } diff --git a/esphome/components/tuya/select/tuya_select.cpp b/esphome/components/tuya/select/tuya_select.cpp index 7b175ee195..d9dc532771 100644 --- a/esphome/components/tuya/select/tuya_select.cpp +++ b/esphome/components/tuya/select/tuya_select.cpp @@ -51,7 +51,7 @@ void TuyaSelect::dump_config() { this->select_id_, this->is_int_ ? "int" : "enum"); const auto &options = this->traits.get_options(); for (size_t i = 0; i < this->mappings_.size(); i++) { - ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options[i]); + ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options.at(i)); } } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 15f05b9b6f..cf21ddc16d 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -322,6 +322,11 @@ template class FixedVector { T &operator[](size_t i) { return data_[i]; } const T &operator[](size_t i) const { return data_[i]; } + /// Access element with bounds checking (matches std::vector behavior) + /// Note: No exception thrown on out of bounds - caller must ensure index is valid + T &at(size_t i) { return data_[i]; } + const T &at(size_t i) const { return data_[i]; } + // Iterator support for range-based for loops T *begin() { return data_; } T *end() { return data_ + size_; } From 44157f1ceddc224c4c5eaf0b6abcd3795c1be930 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 07:16:40 -0700 Subject: [PATCH 074/394] tweak --- esphome/components/modbus_controller/select/modbus_select.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/modbus_controller/select/modbus_select.cpp b/esphome/components/modbus_controller/select/modbus_select.cpp index 674dd05e55..4d4b5a4ffc 100644 --- a/esphome/components/modbus_controller/select/modbus_select.cpp +++ b/esphome/components/modbus_controller/select/modbus_select.cpp @@ -28,7 +28,7 @@ void ModbusSelect::parse_and_publish(const std::vector &data) { if (map_it != this->mapping_.cend()) { size_t idx = std::distance(this->mapping_.cbegin(), map_it); - new_state = this->traits.get_options()[idx]; + new_state = std::string(this->traits.get_options()[idx]); ESP_LOGV(TAG, "Found option %s for value %lld", new_state->c_str(), value); } else { ESP_LOGE(TAG, "No option found for mapping %lld", value); From 353caaf4ffa7ee0b4cbd3db711ea3f04b05709ae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 09:33:38 -0700 Subject: [PATCH 075/394] touch ups --- esphome/components/tuya/select/tuya_select.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/tuya/select/tuya_select.cpp b/esphome/components/tuya/select/tuya_select.cpp index d9dc532771..7c1cd09d06 100644 --- a/esphome/components/tuya/select/tuya_select.cpp +++ b/esphome/components/tuya/select/tuya_select.cpp @@ -10,7 +10,6 @@ void TuyaSelect::setup() { this->parent_->register_listener(this->select_id_, [this](const TuyaDatapoint &datapoint) { uint8_t enum_value = datapoint.value_enum; ESP_LOGV(TAG, "MCU reported select %u value %u", this->select_id_, enum_value); - const auto &options = this->traits.get_options(); auto mappings = this->mappings_; auto it = std::find(mappings.cbegin(), mappings.cend(), enum_value); if (it == mappings.end()) { From 6929bdb4151f6a970b12a357d202d98596ecd115 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 24 Oct 2025 15:01:30 -0400 Subject: [PATCH 076/394] [remote_transmitter] Remove delays and use RMT instead (#11505) --- .../remote_transmitter/remote_transmitter.h | 26 +++- .../remote_transmitter_esp32.cpp | 145 +++++++++++++++++- 2 files changed, 167 insertions(+), 4 deletions(-) diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index aa1f54911d..b5d8e8d83f 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -12,6 +12,25 @@ namespace esphome { namespace remote_transmitter { +#ifdef USE_ESP32 +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) +// IDF version 5.5.1 and above is required because of a bug in +// the RMT encoder: https://github.com/espressif/esp-idf/issues/17244 +typedef union { // NOLINT(modernize-use-using) + struct { + uint16_t duration : 15; + uint16_t level : 1; + }; + uint16_t val; +} rmt_symbol_half_t; + +struct RemoteTransmitterComponentStore { + uint32_t times{0}; + uint32_t index{0}; +}; +#endif +#endif + class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, public Component #ifdef USE_ESP32 @@ -56,9 +75,14 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, #ifdef USE_ESP32 void configure_rmt_(); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) + RemoteTransmitterComponentStore store_{}; + std::vector rmt_temp_; +#else + std::vector rmt_temp_; +#endif uint32_t current_carrier_frequency_{38000}; bool initialized_{false}; - std::vector rmt_temp_; bool with_dma_{false}; bool eot_level_{false}; rmt_channel_handle_t channel_{NULL}; diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp index 119aa81e7e..27bbf3c210 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp @@ -10,6 +10,46 @@ namespace remote_transmitter { static const char *const TAG = "remote_transmitter"; +// Maximum RMT symbol duration (15-bit field) +static constexpr uint32_t RMT_SYMBOL_DURATION_MAX = 0x7FFF; + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) +static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size_t written, size_t free, + rmt_symbol_word_t *symbols, bool *done, void *arg) { + auto *store = static_cast(arg); + const auto *encoded = static_cast(data); + size_t length = size / sizeof(rmt_symbol_half_t); + size_t count = 0; + + // copy symbols + for (size_t i = 0; i < free; i++) { + uint16_t sym_0 = encoded[store->index++].val; + if (store->index >= length) { + store->index = 0; + store->times--; + if (store->times == 0) { + *done = true; + symbols[count++].val = sym_0; + return count; + } + } + uint16_t sym_1 = encoded[store->index++].val; + if (store->index >= length) { + store->index = 0; + store->times--; + if (store->times == 0) { + *done = true; + symbols[count++].val = sym_0 | (sym_1 << 16); + return count; + } + } + symbols[count++].val = sym_0 | (sym_1 << 16); + } + *done = false; + return count; +} +#endif + void RemoteTransmitterComponent::setup() { this->inverted_ = this->pin_->is_inverted(); this->configure_rmt_(); @@ -34,6 +74,17 @@ void RemoteTransmitterComponent::dump_config() { } void RemoteTransmitterComponent::digital_write(bool value) { +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) + rmt_symbol_half_t symbol = { + .duration = 1, + .level = value, + }; + rmt_transmit_config_t config; + memset(&config, 0, sizeof(config)); + config.flags.eot_level = value; + this->store_.times = 1; + this->store_.index = 0; +#else rmt_symbol_word_t symbol = { .duration0 = 1, .level0 = value, @@ -42,8 +93,8 @@ void RemoteTransmitterComponent::digital_write(bool value) { }; rmt_transmit_config_t config; memset(&config, 0, sizeof(config)); - config.loop_count = 0; config.flags.eot_level = value; +#endif esp_err_t error = rmt_transmit(this->channel_, this->encoder_, &symbol, sizeof(symbol), &config); if (error != ESP_OK) { ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error)); @@ -90,6 +141,20 @@ void RemoteTransmitterComponent::configure_rmt_() { gpio_pullup_dis(gpio_num_t(this->pin_->get_pin())); } +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) + rmt_simple_encoder_config_t encoder; + memset(&encoder, 0, sizeof(encoder)); + encoder.callback = encoder_callback; + encoder.arg = &this->store_; + encoder.min_chunk_size = 1; + error = rmt_new_simple_encoder(&encoder, &this->encoder_); + if (error != ESP_OK) { + this->error_code_ = error; + this->error_string_ = "in rmt_new_simple_encoder"; + this->mark_failed(); + return; + } +#else rmt_copy_encoder_config_t encoder; memset(&encoder, 0, sizeof(encoder)); error = rmt_new_copy_encoder(&encoder, &this->encoder_); @@ -99,6 +164,7 @@ void RemoteTransmitterComponent::configure_rmt_() { this->mark_failed(); return; } +#endif error = rmt_enable(this->channel_); if (error != ESP_OK) { @@ -130,6 +196,79 @@ void RemoteTransmitterComponent::configure_rmt_() { } } +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) +void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { + if (this->is_failed()) { + return; + } + + if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) { + this->current_carrier_frequency_ = this->temp_.get_carrier_frequency(); + this->configure_rmt_(); + } + + this->rmt_temp_.clear(); + this->rmt_temp_.reserve(this->temp_.get_data().size() + 1); + + // encode any delay at the start of the buffer to simplify the encoder callback + // this will be skipped the first time around + send_wait = this->from_microseconds_(static_cast(send_wait)); + while (send_wait > 0) { + int32_t duration = std::min(send_wait, uint32_t(RMT_SYMBOL_DURATION_MAX)); + this->rmt_temp_.push_back({ + .duration = static_cast(duration), + .level = static_cast(this->eot_level_), + }); + send_wait -= duration; + } + + // encode data + size_t offset = this->rmt_temp_.size(); + for (int32_t value : this->temp_.get_data()) { + bool level = value >= 0; + if (!level) { + value = -value; + } + value = this->from_microseconds_(static_cast(value)); + while (value > 0) { + int32_t duration = std::min(value, int32_t(RMT_SYMBOL_DURATION_MAX)); + this->rmt_temp_.push_back({ + .duration = static_cast(duration), + .level = static_cast(level ^ this->inverted_), + }); + value -= duration; + } + } + + if ((this->rmt_temp_.data() == nullptr) || this->rmt_temp_.size() <= offset) { + ESP_LOGE(TAG, "Empty data"); + return; + } + + this->transmit_trigger_->trigger(); + + rmt_transmit_config_t config; + memset(&config, 0, sizeof(config)); + config.flags.eot_level = this->eot_level_; + this->store_.times = send_times; + this->store_.index = offset; + esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_temp_.data(), + this->rmt_temp_.size() * sizeof(rmt_symbol_half_t), &config); + if (error != ESP_OK) { + ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error)); + this->status_set_warning(); + } else { + this->status_clear_warning(); + } + error = rmt_tx_wait_all_done(this->channel_, -1); + if (error != ESP_OK) { + ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error)); + this->status_set_warning(); + } + + this->complete_trigger_->trigger(); +} +#else void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { if (this->is_failed()) return; @@ -151,7 +290,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen val = this->from_microseconds_(static_cast(val)); do { - int32_t item = std::min(val, int32_t(32767)); + int32_t item = std::min(val, int32_t(RMT_SYMBOL_DURATION_MAX)); val -= item; if (rmt_i % 2 == 0) { @@ -180,7 +319,6 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen for (uint32_t i = 0; i < send_times; i++) { rmt_transmit_config_t config; memset(&config, 0, sizeof(config)); - config.loop_count = 0; config.flags.eot_level = this->eot_level_; esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_temp_.data(), this->rmt_temp_.size() * sizeof(rmt_symbol_word_t), &config); @@ -200,6 +338,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen } this->complete_trigger_->trigger(); } +#endif } // namespace remote_transmitter } // namespace esphome From 7f06e0bbca36e151644a16d3b52500695f3484db Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 13:32:18 -0700 Subject: [PATCH 077/394] [template] Store initial option as index in template select --- .../components/template/select/__init__.py | 3 +- .../template/select/template_select.cpp | 30 +++++++++---------- .../template/select/template_select.h | 4 +-- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py index 3282092d63..b998a1c2c7 100644 --- a/esphome/components/template/select/__init__.py +++ b/esphome/components/template/select/__init__.py @@ -74,7 +74,8 @@ async def to_code(config): else: cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) - cg.add(var.set_initial_option(config[CONF_INITIAL_OPTION])) + initial_option_index = config[CONF_OPTIONS].index(config[CONF_INITIAL_OPTION]) + cg.add(var.set_initial_option_index(initial_option_index)) if CONF_RESTORE_VALUE in config: cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 95b0ee0d2b..a07215e77e 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -10,26 +10,21 @@ void TemplateSelect::setup() { if (this->f_.has_value()) return; - std::string value; - if (!this->restore_value_) { - value = this->initial_option_; - ESP_LOGD(TAG, "State from initial: %s", value.c_str()); - } else { - size_t index; + size_t index = this->initial_option_index_; + if (this->restore_value_) { this->pref_ = global_preferences->make_preference(this->get_preference_hash()); - if (!this->pref_.load(&index)) { - value = this->initial_option_; - ESP_LOGD(TAG, "State from initial (could not load stored index): %s", value.c_str()); - } else if (!this->has_index(index)) { - value = this->initial_option_; - ESP_LOGD(TAG, "State from initial (restored index %d out of bounds): %s", index, value.c_str()); + size_t restored_index; + if (this->pref_.load(&restored_index) && this->has_index(restored_index)) { + index = restored_index; + ESP_LOGD(TAG, "State from restore: %s", this->at(index).value().c_str()); } else { - value = this->at(index).value(); - ESP_LOGD(TAG, "State from restore: %s", value.c_str()); + ESP_LOGD(TAG, "State from initial (could not load or invalid stored index): %s", this->at(index).value().c_str()); } + } else { + ESP_LOGD(TAG, "State from initial: %s", this->at(index).value().c_str()); } - this->publish_state(value); + this->publish_state(this->at(index).value()); } void TemplateSelect::update() { @@ -65,11 +60,14 @@ void TemplateSelect::dump_config() { LOG_UPDATE_INTERVAL(this); if (this->f_.has_value()) return; + auto initial_option = this->at(this->initial_option_index_); ESP_LOGCONFIG(TAG, " Optimistic: %s\n" " Initial Option: %s\n" " Restore Value: %s", - YESNO(this->optimistic_), this->initial_option_.c_str(), YESNO(this->restore_value_)); + YESNO(this->optimistic_), + initial_option.has_value() ? initial_option.value().c_str() : LOG_STR_LITERAL("unknown"), + YESNO(this->restore_value_)); } } // namespace template_ diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 2f00765c3d..d46ce38314 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -19,13 +19,13 @@ class TemplateSelect : public select::Select, public PollingComponent { Trigger *get_set_trigger() const { return this->set_trigger_; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } - void set_initial_option(const std::string &initial_option) { this->initial_option_ = initial_option; } + void set_initial_option_index(size_t initial_option_index) { this->initial_option_index_ = initial_option_index; } void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } protected: void control(const std::string &value) override; bool optimistic_ = false; - std::string initial_option_; + size_t initial_option_index_{0}; bool restore_value_ = false; Trigger *set_trigger_ = new Trigger(); optional()>> f_; From 7efa1f7641062bc0f0592793d08cf7f16a3396b2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 13:39:06 -0700 Subject: [PATCH 078/394] test --- .../host_mode_empty_string_options.yaml | 11 ++++++++ .../test_host_mode_empty_string_options.py | 28 +++++++++++++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/tests/integration/fixtures/host_mode_empty_string_options.yaml b/tests/integration/fixtures/host_mode_empty_string_options.yaml index ab8e6cd005..a170511c46 100644 --- a/tests/integration/fixtures/host_mode_empty_string_options.yaml +++ b/tests/integration/fixtures/host_mode_empty_string_options.yaml @@ -41,6 +41,17 @@ select: - "" # Empty string at the end initial_option: "Choice X" + - platform: template + name: "Select Initial Option Test" + id: select_initial_option_test + optimistic: true + options: + - "First" + - "Second" + - "Third" + - "Fourth" + initial_option: "Third" # Test non-default initial option + # Add a sensor to ensure we have other entities in the list sensor: - platform: template diff --git a/tests/integration/test_host_mode_empty_string_options.py b/tests/integration/test_host_mode_empty_string_options.py index 242db2d40f..1316e43a9f 100644 --- a/tests/integration/test_host_mode_empty_string_options.py +++ b/tests/integration/test_host_mode_empty_string_options.py @@ -36,8 +36,8 @@ async def test_host_mode_empty_string_options( # Find our select entities select_entities = [e for e in entity_info if isinstance(e, SelectInfo)] - assert len(select_entities) == 3, ( - f"Expected 3 select entities, got {len(select_entities)}" + assert len(select_entities) == 4, ( + f"Expected 4 select entities, got {len(select_entities)}" ) # Verify each select entity by name and check their options @@ -71,6 +71,15 @@ async def test_host_mode_empty_string_options( assert empty_last.options[2] == "Choice Z" assert empty_last.options[3] == "" # Empty string at end + # Check "Select Initial Option Test" - verify non-default initial option + assert "Select Initial Option Test" in selects_by_name + initial_option_test = selects_by_name["Select Initial Option Test"] + assert len(initial_option_test.options) == 4 + assert initial_option_test.options[0] == "First" + assert initial_option_test.options[1] == "Second" + assert initial_option_test.options[2] == "Third" + assert initial_option_test.options[3] == "Fourth" + # If we got here without protobuf decoding errors, the fix is working # The bug would have caused "Invalid protobuf message" errors with trailing bytes @@ -78,7 +87,12 @@ async def test_host_mode_empty_string_options( # This ensures empty strings work properly in state messages too states: dict[int, EntityState] = {} states_received_future: asyncio.Future[None] = loop.create_future() - expected_select_keys = {empty_first.key, empty_middle.key, empty_last.key} + expected_select_keys = { + empty_first.key, + empty_middle.key, + empty_last.key, + initial_option_test.key, + } received_select_keys = set() def on_state(state: EntityState) -> None: @@ -109,6 +123,14 @@ async def test_host_mode_empty_string_options( assert empty_first.key in states assert empty_middle.key in states assert empty_last.key in states + assert initial_option_test.key in states + + # Verify the initial option is set correctly to "Third" (not the default "First") + initial_state = states[initial_option_test.key] + assert initial_state.state == "Third", ( + f"Expected initial state 'Third' but got '{initial_state.state}' - " + f"initial_option_index optimization may not be working correctly" + ) # The main test is that we got here without protobuf errors # The select entities with empty string options were properly encoded From 45c24e95508fc18ffdf046466b570abaac34167f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 14:09:59 -0700 Subject: [PATCH 079/394] [sntp] Store server strings in flash memory --- esphome/components/sntp/sntp_component.cpp | 16 ++++++++++--- esphome/components/sntp/sntp_component.h | 22 +++++++++++++----- esphome/components/sntp/time.py | 26 +++++++++++++++++++++- 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index 1cca5e8043..1457045d29 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -27,7 +27,7 @@ void SNTPComponent::setup() { esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL); size_t i = 0; for (auto &server : this->servers_) { - esp_sntp_setservername(i++, server.c_str()); + esp_sntp_setservername(i++, server); } esp_sntp_set_sync_interval(this->get_update_interval()); esp_sntp_set_time_sync_notification_cb([](struct timeval *tv) { @@ -42,7 +42,16 @@ void SNTPComponent::setup() { size_t i = 0; for (auto &server : this->servers_) { - sntp_setservername(i++, server.c_str()); +#if defined(USE_ESP8266) + // On ESP8266, server is PGM_P pointing to PROGMEM + // LWIP's sntp_setservername is not PROGMEM-aware, so copy to stack buffer first + char server_buf[64]; + strncpy_P(server_buf, server, sizeof(server_buf) - 1); + server_buf[sizeof(server_buf) - 1] = '\0'; + sntp_setservername(i++, server_buf); +#else + sntp_setservername(i++, server); +#endif } #if defined(USE_ESP8266) @@ -59,7 +68,8 @@ void SNTPComponent::dump_config() { ESP_LOGCONFIG(TAG, "SNTP Time:"); size_t i = 0; for (auto &server : this->servers_) { - ESP_LOGCONFIG(TAG, " Server %zu: '%s'", i++, server.c_str()); + // LOG_STR_ARG handles both PROGMEM (ESP8266) and regular pointers + ESP_LOGCONFIG(TAG, " Server %zu: '%s'", i++, LOG_STR_ARG(server)); } } void SNTPComponent::update() { diff --git a/esphome/components/sntp/sntp_component.h b/esphome/components/sntp/sntp_component.h index dd4c71e082..a320bf474c 100644 --- a/esphome/components/sntp/sntp_component.h +++ b/esphome/components/sntp/sntp_component.h @@ -2,10 +2,18 @@ #include "esphome/core/component.h" #include "esphome/components/time/real_time_clock.h" +#include + +#ifdef USE_ESP8266 +#include +#endif namespace esphome { namespace sntp { +// Server count is calculated at compile time by Python codegen +// SNTP_SERVER_COUNT will always be defined + /// The SNTP component allows you to configure local timekeeping via Simple Network Time Protocol. /// /// \note @@ -14,10 +22,7 @@ namespace sntp { /// \see https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html class SNTPComponent : public time::RealTimeClock { public: - SNTPComponent(const std::vector &servers) : servers_(servers) {} - - // Note: set_servers() has been removed and replaced by a constructor - calling set_servers after setup would - // have had no effect anyway, and making the strings immutable avoids the need to strdup their contents. + SNTPComponent() = default; void setup() override; void dump_config() override; @@ -28,8 +33,15 @@ class SNTPComponent : public time::RealTimeClock { void time_synced(); +#ifdef USE_ESP8266 + // On ESP8266, store pointers to PROGMEM strings to save RAM + std::array servers_{}; +#else + // On other platforms, store regular const char pointers + std::array servers_{}; +#endif + protected: - std::vector servers_; bool has_time_{false}; #if defined(USE_ESP32) diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py index 1c8ee402ad..7571cf198b 100644 --- a/esphome/components/sntp/time.py +++ b/esphome/components/sntp/time.py @@ -12,6 +12,7 @@ from esphome.const import ( PLATFORM_RTL87XX, ) from esphome.core import CORE +from esphome.cpp_generator import ProgmemAssignmentExpression DEPENDENCIES = ["network"] sntp_ns = cg.esphome_ns.namespace("sntp") @@ -43,11 +44,34 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): servers = config[CONF_SERVERS] - var = cg.new_Pvariable(config[CONF_ID], servers) + server_count = len(servers) + + # Define server count at compile time + cg.add_define("SNTP_SERVER_COUNT", server_count) + + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await time_.register_time(var, config) + # Generate PROGMEM strings for ESP8266, regular strings for other platforms + if CORE.is_esp8266: + # On ESP8266, use PROGMEM to store strings in flash + # Use ProgmemAssignmentExpression to generate: static const char name[] PROGMEM = "value"; + for i, server in enumerate(servers): + var_name = f"{config[CONF_ID].id}_server_{i}" + # Create PROGMEM string: static const char var_name[] PROGMEM = "server"; + assignment = ProgmemAssignmentExpression( + "char", var_name, cg.safe_exp(server) + ) + cg.add(assignment) + # Assign pointer to array element + cg.add(cg.RawStatement(f"{var}->servers_[{i}] = {var_name};")) + else: + # On other platforms, use regular string literals + for i, server in enumerate(servers): + cg.add(cg.RawStatement(f"{var}->servers_[{i}] = {cg.safe_exp(server)};")) + if CORE.is_esp8266 and len(servers) > 1: # We need LwIP features enabled to get 3 SNTP servers (not just one) cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY") From 45770811d22f395f5779c9a4bfef2349f6819da2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 14:13:41 -0700 Subject: [PATCH 080/394] [sntp] Store server strings in flash memory --- esphome/core/defines.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 39698c1004..8095ffed4a 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -87,6 +87,7 @@ #define USE_MDNS_STORE_SERVICES #define MDNS_SERVICE_COUNT 3 #define MDNS_DYNAMIC_TXT_COUNT 3 +#define SNTP_SERVER_COUNT 3 #define USE_MEDIA_PLAYER #define USE_NEXTION_TFT_UPLOAD #define USE_NUMBER From 54fb391f13a72a76fe5b0ddfd0d821d62fc00b0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 14:26:17 -0700 Subject: [PATCH 081/394] cleanup --- esphome/components/sntp/sntp_component.h | 9 ++++----- esphome/components/sntp/time.py | 22 +++++++++++----------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/esphome/components/sntp/sntp_component.h b/esphome/components/sntp/sntp_component.h index a320bf474c..d5ce25e8ee 100644 --- a/esphome/components/sntp/sntp_component.h +++ b/esphome/components/sntp/sntp_component.h @@ -22,7 +22,7 @@ namespace sntp { /// \see https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html class SNTPComponent : public time::RealTimeClock { public: - SNTPComponent() = default; + template SNTPComponent(Args... servers) : servers_{servers...} {} void setup() override; void dump_config() override; @@ -33,15 +33,14 @@ class SNTPComponent : public time::RealTimeClock { void time_synced(); + protected: #ifdef USE_ESP8266 // On ESP8266, store pointers to PROGMEM strings to save RAM - std::array servers_{}; + std::array servers_; #else // On other platforms, store regular const char pointers - std::array servers_{}; + std::array servers_; #endif - - protected: bool has_time_{false}; #if defined(USE_ESP32) diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py index 7571cf198b..78b586ea5a 100644 --- a/esphome/components/sntp/time.py +++ b/esphome/components/sntp/time.py @@ -49,15 +49,10 @@ async def to_code(config): # Define server count at compile time cg.add_define("SNTP_SERVER_COUNT", server_count) - var = cg.new_Pvariable(config[CONF_ID]) - - await cg.register_component(var, config) - await time_.register_time(var, config) - # Generate PROGMEM strings for ESP8266, regular strings for other platforms if CORE.is_esp8266: # On ESP8266, use PROGMEM to store strings in flash - # Use ProgmemAssignmentExpression to generate: static const char name[] PROGMEM = "value"; + server_vars = [] for i, server in enumerate(servers): var_name = f"{config[CONF_ID].id}_server_{i}" # Create PROGMEM string: static const char var_name[] PROGMEM = "server"; @@ -65,12 +60,17 @@ async def to_code(config): "char", var_name, cg.safe_exp(server) ) cg.add(assignment) - # Assign pointer to array element - cg.add(cg.RawStatement(f"{var}->servers_[{i}] = {var_name};")) + server_vars.append(cg.RawExpression(var_name)) + # Pass PROGMEM string pointers to constructor using ArrayInitializer + var = cg.new_Pvariable(config[CONF_ID], cg.ArrayInitializer(*server_vars)) else: - # On other platforms, use regular string literals - for i, server in enumerate(servers): - cg.add(cg.RawStatement(f"{var}->servers_[{i}] = {cg.safe_exp(server)};")) + # On other platforms, pass regular string literals to constructor + var = cg.new_Pvariable( + config[CONF_ID], cg.ArrayInitializer(*[cg.safe_exp(s) for s in servers]) + ) + + await cg.register_component(var, config) + await time_.register_time(var, config) if CORE.is_esp8266 and len(servers) > 1: # We need LwIP features enabled to get 3 SNTP servers (not just one) From 3025d35554568c4a3813bddf34412c20d023a867 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 14:37:15 -0700 Subject: [PATCH 082/394] must still be in ram on 8266 --- esphome/components/sntp/sntp_component.cpp | 12 +--------- esphome/components/sntp/sntp_component.h | 7 +----- esphome/components/sntp/time.py | 26 +++++----------------- 3 files changed, 8 insertions(+), 37 deletions(-) diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index 1457045d29..331a9b3509 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -42,16 +42,7 @@ void SNTPComponent::setup() { size_t i = 0; for (auto &server : this->servers_) { -#if defined(USE_ESP8266) - // On ESP8266, server is PGM_P pointing to PROGMEM - // LWIP's sntp_setservername is not PROGMEM-aware, so copy to stack buffer first - char server_buf[64]; - strncpy_P(server_buf, server, sizeof(server_buf) - 1); - server_buf[sizeof(server_buf) - 1] = '\0'; - sntp_setservername(i++, server_buf); -#else sntp_setservername(i++, server); -#endif } #if defined(USE_ESP8266) @@ -68,8 +59,7 @@ void SNTPComponent::dump_config() { ESP_LOGCONFIG(TAG, "SNTP Time:"); size_t i = 0; for (auto &server : this->servers_) { - // LOG_STR_ARG handles both PROGMEM (ESP8266) and regular pointers - ESP_LOGCONFIG(TAG, " Server %zu: '%s'", i++, LOG_STR_ARG(server)); + ESP_LOGCONFIG(TAG, " Server %zu: '%s'", i++, server); } } void SNTPComponent::update() { diff --git a/esphome/components/sntp/sntp_component.h b/esphome/components/sntp/sntp_component.h index d5ce25e8ee..bd6877cc5c 100644 --- a/esphome/components/sntp/sntp_component.h +++ b/esphome/components/sntp/sntp_component.h @@ -34,13 +34,8 @@ class SNTPComponent : public time::RealTimeClock { void time_synced(); protected: -#ifdef USE_ESP8266 - // On ESP8266, store pointers to PROGMEM strings to save RAM - std::array servers_; -#else - // On other platforms, store regular const char pointers + // Store const char pointers - compiler stores string literals in flash on all platforms std::array servers_; -#endif bool has_time_{false}; #if defined(USE_ESP32) diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py index 78b586ea5a..afa7f6d1fd 100644 --- a/esphome/components/sntp/time.py +++ b/esphome/components/sntp/time.py @@ -12,7 +12,6 @@ from esphome.const import ( PLATFORM_RTL87XX, ) from esphome.core import CORE -from esphome.cpp_generator import ProgmemAssignmentExpression DEPENDENCIES = ["network"] sntp_ns = cg.esphome_ns.namespace("sntp") @@ -49,25 +48,12 @@ async def to_code(config): # Define server count at compile time cg.add_define("SNTP_SERVER_COUNT", server_count) - # Generate PROGMEM strings for ESP8266, regular strings for other platforms - if CORE.is_esp8266: - # On ESP8266, use PROGMEM to store strings in flash - server_vars = [] - for i, server in enumerate(servers): - var_name = f"{config[CONF_ID].id}_server_{i}" - # Create PROGMEM string: static const char var_name[] PROGMEM = "server"; - assignment = ProgmemAssignmentExpression( - "char", var_name, cg.safe_exp(server) - ) - cg.add(assignment) - server_vars.append(cg.RawExpression(var_name)) - # Pass PROGMEM string pointers to constructor using ArrayInitializer - var = cg.new_Pvariable(config[CONF_ID], cg.ArrayInitializer(*server_vars)) - else: - # On other platforms, pass regular string literals to constructor - var = cg.new_Pvariable( - config[CONF_ID], cg.ArrayInitializer(*[cg.safe_exp(s) for s in servers]) - ) + # Pass string literals to constructor - stored in flash/rodata by compiler + # On ESP8266, LWIP doesn't support PROGMEM pointers, so strings are in rodata (RAM) + # but we still avoid the ~24 byte std::string overhead per server + var = cg.new_Pvariable( + config[CONF_ID], cg.ArrayInitializer(*[cg.safe_exp(s) for s in servers]) + ) await cg.register_component(var, config) await time_.register_time(var, config) From ccdce3508ca330e1c2af7fe187abbc5137a72d46 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 14:37:29 -0700 Subject: [PATCH 083/394] must still be in ram on 8266 --- esphome/components/sntp/sntp_component.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/sntp/sntp_component.h b/esphome/components/sntp/sntp_component.h index bd6877cc5c..d22d7c669d 100644 --- a/esphome/components/sntp/sntp_component.h +++ b/esphome/components/sntp/sntp_component.h @@ -34,7 +34,9 @@ class SNTPComponent : public time::RealTimeClock { void time_synced(); protected: - // Store const char pointers - compiler stores string literals in flash on all platforms + // Store const char pointers to string literals + // ESP8266: strings in rodata (RAM), but avoids std::string overhead (~24 bytes each) + // Other platforms: strings in flash std::array servers_; bool has_time_{false}; From 9e798ffa4f052204978690dd73adb9b6d6826c3b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 14:37:35 -0700 Subject: [PATCH 084/394] must still be in ram on 8266 --- esphome/components/sntp/sntp_component.h | 4 ---- 1 file changed, 4 deletions(-) diff --git a/esphome/components/sntp/sntp_component.h b/esphome/components/sntp/sntp_component.h index d22d7c669d..74c342a38a 100644 --- a/esphome/components/sntp/sntp_component.h +++ b/esphome/components/sntp/sntp_component.h @@ -4,10 +4,6 @@ #include "esphome/components/time/real_time_clock.h" #include -#ifdef USE_ESP8266 -#include -#endif - namespace esphome { namespace sntp { From 01b1844e9dec96d2a5505c8610a74031d31a1ddb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 14:38:46 -0700 Subject: [PATCH 085/394] must still be in ram on 8266 --- esphome/components/sntp/sntp_component.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/sntp/sntp_component.h b/esphome/components/sntp/sntp_component.h index 74c342a38a..de40faa259 100644 --- a/esphome/components/sntp/sntp_component.h +++ b/esphome/components/sntp/sntp_component.h @@ -18,7 +18,7 @@ namespace sntp { /// \see https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html class SNTPComponent : public time::RealTimeClock { public: - template SNTPComponent(Args... servers) : servers_{servers...} {} + SNTPComponent(std::array servers) : servers_(servers) {} void setup() override; void dump_config() override; From 875506f2f7fd938d8f972720269ad8eeb9b9cf63 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 17:30:04 -0700 Subject: [PATCH 086/394] cleanup --- esphome/components/sntp/time.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py index afa7f6d1fd..d27fc9991d 100644 --- a/esphome/components/sntp/time.py +++ b/esphome/components/sntp/time.py @@ -43,17 +43,12 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): servers = config[CONF_SERVERS] - server_count = len(servers) # Define server count at compile time - cg.add_define("SNTP_SERVER_COUNT", server_count) + cg.add_define("SNTP_SERVER_COUNT", len(servers)) # Pass string literals to constructor - stored in flash/rodata by compiler - # On ESP8266, LWIP doesn't support PROGMEM pointers, so strings are in rodata (RAM) - # but we still avoid the ~24 byte std::string overhead per server - var = cg.new_Pvariable( - config[CONF_ID], cg.ArrayInitializer(*[cg.safe_exp(s) for s in servers]) - ) + var = cg.new_Pvariable(config[CONF_ID], servers) await cg.register_component(var, config) await time_.register_time(var, config) From b77db3604faae6c293468a4197779cc62ce96ba0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 17:32:38 -0700 Subject: [PATCH 087/394] cleanup --- esphome/components/sntp/sntp_component.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/sntp/sntp_component.h b/esphome/components/sntp/sntp_component.h index de40faa259..8f2e411c18 100644 --- a/esphome/components/sntp/sntp_component.h +++ b/esphome/components/sntp/sntp_component.h @@ -18,7 +18,7 @@ namespace sntp { /// \see https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html class SNTPComponent : public time::RealTimeClock { public: - SNTPComponent(std::array servers) : servers_(servers) {} + SNTPComponent(const std::array &servers) : servers_(servers) {} void setup() override; void dump_config() override; From 1ea48df6d68fa5188fd08f053c388fa819cdd0fd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 17:40:56 -0700 Subject: [PATCH 088/394] save some bytes --- esphome/components/select/select_traits.cpp | 2 +- esphome/components/select/select_traits.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/select/select_traits.cpp b/esphome/components/select/select_traits.cpp index dc849b8b7e..c6ded98ebf 100644 --- a/esphome/components/select/select_traits.cpp +++ b/esphome/components/select/select_traits.cpp @@ -3,7 +3,7 @@ namespace esphome { namespace select { -void SelectTraits::set_options(std::initializer_list options) { this->options_ = options; } +void SelectTraits::set_options(const std::initializer_list &options) { this->options_ = options; } void SelectTraits::set_options(const FixedVector &options) { this->options_.init(options.size()); diff --git a/esphome/components/select/select_traits.h b/esphome/components/select/select_traits.h index b504f08298..ee59a030ad 100644 --- a/esphome/components/select/select_traits.h +++ b/esphome/components/select/select_traits.h @@ -8,7 +8,7 @@ namespace select { class SelectTraits { public: - void set_options(std::initializer_list options); + void set_options(const std::initializer_list &options); void set_options(const FixedVector &options); const FixedVector &get_options() const; From 5fdd90c71a4479992dbb93460afd3f6ae89aeaab Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sat, 25 Oct 2025 03:27:39 -0400 Subject: [PATCH 089/394] [esp32] Add IDF 5.4.3 to platform list and switch to tar.xz (#11528) --- esphome/components/esp32/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 48d11f46fa..ddb8dbb1f0 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -304,9 +304,13 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: def _format_framework_espidf_version(ver: cv.Version, release: str) -> str: # format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to # a PIO platformio/framework-espidf value + if ver == cv.Version(5, 4, 3) or ver >= cv.Version(5, 5, 1): + ext = "tar.xz" + else: + ext = "zip" if release: - return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.zip" - return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip" + return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.{ext}" + return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.{ext}" def _is_framework_url(source: str) -> str: @@ -355,6 +359,7 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { ESP_IDF_PLATFORM_VERSION_LOOKUP = { cv.Version(5, 5, 1): cv.Version(55, 3, 31, "1"), cv.Version(5, 5, 0): cv.Version(55, 3, 31, "1"), + cv.Version(5, 4, 3): cv.Version(55, 3, 32), cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"), cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"), cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"), From e212ed024d80bf6b03b0ce5786ca5c294f791e27 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 10:00:43 -0700 Subject: [PATCH 090/394] [sntp] Replace std::vector with std::array to save heap memory (#11525) --- esphome/components/sntp/sntp_component.cpp | 6 +++--- esphome/components/sntp/sntp_component.h | 14 +++++++++----- esphome/components/sntp/time.py | 5 +++++ esphome/core/defines.h | 1 + 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index 1cca5e8043..331a9b3509 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -27,7 +27,7 @@ void SNTPComponent::setup() { esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL); size_t i = 0; for (auto &server : this->servers_) { - esp_sntp_setservername(i++, server.c_str()); + esp_sntp_setservername(i++, server); } esp_sntp_set_sync_interval(this->get_update_interval()); esp_sntp_set_time_sync_notification_cb([](struct timeval *tv) { @@ -42,7 +42,7 @@ void SNTPComponent::setup() { size_t i = 0; for (auto &server : this->servers_) { - sntp_setservername(i++, server.c_str()); + sntp_setservername(i++, server); } #if defined(USE_ESP8266) @@ -59,7 +59,7 @@ void SNTPComponent::dump_config() { ESP_LOGCONFIG(TAG, "SNTP Time:"); size_t i = 0; for (auto &server : this->servers_) { - ESP_LOGCONFIG(TAG, " Server %zu: '%s'", i++, server.c_str()); + ESP_LOGCONFIG(TAG, " Server %zu: '%s'", i++, server); } } void SNTPComponent::update() { diff --git a/esphome/components/sntp/sntp_component.h b/esphome/components/sntp/sntp_component.h index dd4c71e082..8f2e411c18 100644 --- a/esphome/components/sntp/sntp_component.h +++ b/esphome/components/sntp/sntp_component.h @@ -2,10 +2,14 @@ #include "esphome/core/component.h" #include "esphome/components/time/real_time_clock.h" +#include namespace esphome { namespace sntp { +// Server count is calculated at compile time by Python codegen +// SNTP_SERVER_COUNT will always be defined + /// The SNTP component allows you to configure local timekeeping via Simple Network Time Protocol. /// /// \note @@ -14,10 +18,7 @@ namespace sntp { /// \see https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html class SNTPComponent : public time::RealTimeClock { public: - SNTPComponent(const std::vector &servers) : servers_(servers) {} - - // Note: set_servers() has been removed and replaced by a constructor - calling set_servers after setup would - // have had no effect anyway, and making the strings immutable avoids the need to strdup their contents. + SNTPComponent(const std::array &servers) : servers_(servers) {} void setup() override; void dump_config() override; @@ -29,7 +30,10 @@ class SNTPComponent : public time::RealTimeClock { void time_synced(); protected: - std::vector servers_; + // Store const char pointers to string literals + // ESP8266: strings in rodata (RAM), but avoids std::string overhead (~24 bytes each) + // Other platforms: strings in flash + std::array servers_; bool has_time_{false}; #if defined(USE_ESP32) diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py index 1c8ee402ad..d27fc9991d 100644 --- a/esphome/components/sntp/time.py +++ b/esphome/components/sntp/time.py @@ -43,6 +43,11 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): servers = config[CONF_SERVERS] + + # Define server count at compile time + cg.add_define("SNTP_SERVER_COUNT", len(servers)) + + # Pass string literals to constructor - stored in flash/rodata by compiler var = cg.new_Pvariable(config[CONF_ID], servers) await cg.register_component(var, config) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 39698c1004..8095ffed4a 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -87,6 +87,7 @@ #define USE_MDNS_STORE_SERVICES #define MDNS_SERVICE_COUNT 3 #define MDNS_DYNAMIC_TXT_COUNT 3 +#define SNTP_SERVER_COUNT 3 #define USE_MEDIA_PLAYER #define USE_NEXTION_TFT_UPLOAD #define USE_NUMBER From 16130308f908d6a41c39a4669f68a73edee89077 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 10:26:53 -0700 Subject: [PATCH 091/394] touch ups --- esphome/components/template/select/template_select.cpp | 4 +--- tests/integration/test_host_mode_empty_string_options.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index a07215e77e..3765cf02bf 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -60,13 +60,11 @@ void TemplateSelect::dump_config() { LOG_UPDATE_INTERVAL(this); if (this->f_.has_value()) return; - auto initial_option = this->at(this->initial_option_index_); ESP_LOGCONFIG(TAG, " Optimistic: %s\n" " Initial Option: %s\n" " Restore Value: %s", - YESNO(this->optimistic_), - initial_option.has_value() ? initial_option.value().c_str() : LOG_STR_LITERAL("unknown"), + YESNO(this->optimistic_), this->at(this->initial_option_index_).value().c_str(), YESNO(this->restore_value_)); } diff --git a/tests/integration/test_host_mode_empty_string_options.py b/tests/integration/test_host_mode_empty_string_options.py index 1316e43a9f..1180ce75fc 100644 --- a/tests/integration/test_host_mode_empty_string_options.py +++ b/tests/integration/test_host_mode_empty_string_options.py @@ -129,7 +129,7 @@ async def test_host_mode_empty_string_options( initial_state = states[initial_option_test.key] assert initial_state.state == "Third", ( f"Expected initial state 'Third' but got '{initial_state.state}' - " - f"initial_option_index optimization may not be working correctly" + f"initial_option not correctly applied" ) # The main test is that we got here without protobuf errors From 3a491035845dc84f44b960c95c997b5a0f1a8ed1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 10:31:13 -0700 Subject: [PATCH 092/394] touch ups --- esphome/components/template/select/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py index b998a1c2c7..93aa2c8b05 100644 --- a/esphome/components/template/select/__init__.py +++ b/esphome/components/template/select/__init__.py @@ -75,7 +75,10 @@ async def to_code(config): else: cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) initial_option_index = config[CONF_OPTIONS].index(config[CONF_INITIAL_OPTION]) - cg.add(var.set_initial_option_index(initial_option_index)) + # Only set if non-zero to avoid bloating setup() function + # (initial_option_index_ is zero-initialized in the header) + if initial_option_index != 0: + cg.add(var.set_initial_option_index(initial_option_index)) if CONF_RESTORE_VALUE in config: cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) From 6c9f93fbf807e1bda8a9aac992fe82fa66340d37 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 10:40:05 -0700 Subject: [PATCH 093/394] touch ups --- esphome/components/template/select/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py index 93aa2c8b05..40ba557d9b 100644 --- a/esphome/components/template/select/__init__.py +++ b/esphome/components/template/select/__init__.py @@ -73,15 +73,18 @@ async def to_code(config): cg.add(var.set_template(template_)) else: - cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + # Only set if non-default to avoid bloating setup() function + if config[CONF_OPTIMISTIC]: + cg.add(var.set_optimistic(True)) initial_option_index = config[CONF_OPTIONS].index(config[CONF_INITIAL_OPTION]) # Only set if non-zero to avoid bloating setup() function # (initial_option_index_ is zero-initialized in the header) if initial_option_index != 0: cg.add(var.set_initial_option_index(initial_option_index)) - if CONF_RESTORE_VALUE in config: - cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) + # Only set if True (default is False) + if CONF_RESTORE_VALUE in config and config[CONF_RESTORE_VALUE]: + cg.add(var.set_restore_value(True)) if CONF_SET_ACTION in config: await automation.build_automation( From f0aa530069937660131f8feeedc04acf2290317a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 10:42:20 -0700 Subject: [PATCH 094/394] preen --- esphome/components/template/select/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py index 40ba557d9b..0e9c240547 100644 --- a/esphome/components/template/select/__init__.py +++ b/esphome/components/template/select/__init__.py @@ -83,7 +83,7 @@ async def to_code(config): cg.add(var.set_initial_option_index(initial_option_index)) # Only set if True (default is False) - if CONF_RESTORE_VALUE in config and config[CONF_RESTORE_VALUE]: + if config.get(CONF_RESTORE_VALUE): cg.add(var.set_restore_value(True)) if CONF_SET_ACTION in config: From 1e220e9803088068d0ed1a8ff32eea0095468ad3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 10:51:26 -0700 Subject: [PATCH 095/394] [number] Skip set_mode call when using default AUTO mode --- esphome/components/number/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 230c3aa0c1..ac0329fcc6 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -252,7 +252,10 @@ async def setup_number_core_( cg.add(var.traits.set_max_value(max_value)) cg.add(var.traits.set_step(step)) - cg.add(var.traits.set_mode(config[CONF_MODE])) + # Only set if non-default to avoid bloating setup() function + # (mode_ is initialized to NUMBER_MODE_AUTO in the header) + if config[CONF_MODE] != NumberMode.NUMBER_MODE_AUTO: + cg.add(var.traits.set_mode(config[CONF_MODE])) for conf in config.get(CONF_ON_VALUE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) From 683ea5c5680258d870234e802092cec488530852 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 11:03:44 -0700 Subject: [PATCH 096/394] [gpio] Skip set_inverted() call for default false value --- esphome/components/esp32/gpio.py | 5 ++++- esphome/components/esp8266/gpio.py | 5 ++++- esphome/components/host/gpio.py | 5 ++++- esphome/components/libretiny/gpio.py | 5 ++++- esphome/components/nrf52/gpio.py | 5 ++++- esphome/components/rp2040/gpio.py | 5 ++++- 6 files changed, 24 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index 513f463d57..954891ea8d 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -223,7 +223,10 @@ async def esp32_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}"))) - cg.add(var.set_inverted(config[CONF_INVERTED])) + # Only set if true to avoid bloating setup() function + # (inverted bit in pin_flags_ bitfield is zero-initialized to false) + if config[CONF_INVERTED]: + cg.add(var.set_inverted(True)) if CONF_DRIVE_STRENGTH in config: cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH])) cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index e7492fc505..2e8d6496bc 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -165,7 +165,10 @@ async def esp8266_pin_to_code(config): num = config[CONF_NUMBER] mode = config[CONF_MODE] cg.add(var.set_pin(num)) - cg.add(var.set_inverted(config[CONF_INVERTED])) + # Only set if true to avoid bloating setup() function + # (inverted bit in pin_flags_ bitfield is zero-initialized to false) + if config[CONF_INVERTED]: + cg.add(var.set_inverted(True)) cg.add(var.set_flags(pins.gpio_flags_expr(mode))) if num < 16: initial_state: PinInitialState = CORE.data[KEY_ESP8266][KEY_PIN_INITIAL_STATES][ diff --git a/esphome/components/host/gpio.py b/esphome/components/host/gpio.py index 0f22a790bd..fcfb0b6c54 100644 --- a/esphome/components/host/gpio.py +++ b/esphome/components/host/gpio.py @@ -57,6 +57,9 @@ async def host_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(num)) - cg.add(var.set_inverted(config[CONF_INVERTED])) + # Only set if true to avoid bloating setup() function + # (inverted bit in pin_flags_ bitfield is zero-initialized to false) + if config[CONF_INVERTED]: + cg.add(var.set_inverted(True)) cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) return var diff --git a/esphome/components/libretiny/gpio.py b/esphome/components/libretiny/gpio.py index 07eb0ce133..9bad400eb7 100644 --- a/esphome/components/libretiny/gpio.py +++ b/esphome/components/libretiny/gpio.py @@ -199,6 +199,9 @@ async def component_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(num)) - cg.add(var.set_inverted(config[CONF_INVERTED])) + # Only set if true to avoid bloating setup() function + # (inverted bit in pin_flags_ bitfield is zero-initialized to false) + if config[CONF_INVERTED]: + cg.add(var.set_inverted(True)) cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) return var diff --git a/esphome/components/nrf52/gpio.py b/esphome/components/nrf52/gpio.py index 260114f90e..17329042b2 100644 --- a/esphome/components/nrf52/gpio.py +++ b/esphome/components/nrf52/gpio.py @@ -74,6 +74,9 @@ async def nrf52_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(num)) - cg.add(var.set_inverted(config[CONF_INVERTED])) + # Only set if true to avoid bloating setup() function + # (inverted bit in pin_flags_ bitfield is zero-initialized to false) + if config[CONF_INVERTED]: + cg.add(var.set_inverted(True)) cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) return var diff --git a/esphome/components/rp2040/gpio.py b/esphome/components/rp2040/gpio.py index 58514f7db5..193e567d17 100644 --- a/esphome/components/rp2040/gpio.py +++ b/esphome/components/rp2040/gpio.py @@ -94,6 +94,9 @@ async def rp2040_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(num)) - cg.add(var.set_inverted(config[CONF_INVERTED])) + # Only set if true to avoid bloating setup() function + # (inverted bit in pin_flags_ bitfield is zero-initialized to false) + if config[CONF_INVERTED]: + cg.add(var.set_inverted(True)) cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) return var From 5861cf37f96628810e59526deeb8f386c66a391e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 11:20:06 -0700 Subject: [PATCH 097/394] [core] Simplify ESPTime::strftime() and save 20 bytes flash --- esphome/core/time.cpp | 22 ++++++++-------------- esphome/core/time.h | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index 1285ec6448..9a1a0dc492 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -46,24 +46,18 @@ struct tm ESPTime::to_c_tm() { return c_tm; } -std::string ESPTime::strftime(const std::string &format) { - std::string timestr; - timestr.resize(format.size() * 4); +std::string ESPTime::strftime(const char *format, size_t format_len) { struct tm c_tm = this->to_c_tm(); - size_t len = ::strftime(×tr[0], timestr.size(), format.c_str(), &c_tm); - while (len == 0) { - if (timestr.size() >= 128) { - // strftime has failed for reasons unrelated to the size of the buffer - // so return a formatting error - return "ERROR"; - } - timestr.resize(timestr.size() * 2); - len = ::strftime(×tr[0], timestr.size(), format.c_str(), &c_tm); + char buf[128]; + size_t len = ::strftime(buf, sizeof(buf), format, &c_tm); + if (len > 0) { + return std::string(buf, len); } - timestr.resize(len); - return timestr; + return "ERROR"; } +std::string ESPTime::strftime(const std::string &format) { return this->strftime(format.c_str(), format.size()); } + bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) { uint16_t year; uint8_t month; diff --git a/esphome/core/time.h b/esphome/core/time.h index a53fca2346..0d47ce820b 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -55,6 +55,23 @@ struct ESPTime { */ std::string strftime(const std::string &format); + /** Convert this ESPTime struct to a string as specified by the format argument. + * @see https://www.gnu.org/software/libc/manual/html_node/Formatting-Calendar-Time.html#index-strftime + * + * This overload is optimized for string literals and avoids std::string parameter overhead. + * + * @param format The format string (null-terminated C string) + * @param format_len Optional length of the format string. If 0 (default), strlen() will be called. + * + * @warning This method uses dynamically allocated strings which can cause heap fragmentation with some + * microcontrollers. + * + * @warning This method can return "ERROR" when the underlying strftime() call fails, e.g. when the + * format string contains unsupported specifiers or when the format string doesn't produce any + * output. + */ + std::string strftime(const char *format, size_t format_len = 0); + /// Check if this ESPTime is valid (all fields in range and year is greater than 2018) bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); } From 960c80b202293c6b36377afffc872b35fae6fd82 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 11:21:22 -0700 Subject: [PATCH 098/394] [core] Simplify ESPTime::strftime() and save 20 bytes flash --- esphome/core/time.h | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/esphome/core/time.h b/esphome/core/time.h index 0d47ce820b..13a0127156 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -44,32 +44,14 @@ struct ESPTime { size_t strftime(char *buffer, size_t buffer_len, const char *format); /** Convert this ESPTime struct to a string as specified by the format argument. - * @see https://www.gnu.org/software/libc/manual/html_node/Formatting-Calendar-Time.html#index-strftime + * @see https://en.cppreference.com/w/c/chrono/strftime * - * @warning This method uses dynamically allocated strings which can cause heap fragmentation with some - * microcontrollers. - * - * @warning This method can return "ERROR" when the underlying strftime() call fails, e.g. when the - * format string contains unsupported specifiers or when the format string doesn't produce any - * output. + * @warning This method can return "ERROR" when the underlying strftime() call fails or when the + * output exceeds 128 bytes. */ std::string strftime(const std::string &format); - /** Convert this ESPTime struct to a string as specified by the format argument. - * @see https://www.gnu.org/software/libc/manual/html_node/Formatting-Calendar-Time.html#index-strftime - * - * This overload is optimized for string literals and avoids std::string parameter overhead. - * - * @param format The format string (null-terminated C string) - * @param format_len Optional length of the format string. If 0 (default), strlen() will be called. - * - * @warning This method uses dynamically allocated strings which can cause heap fragmentation with some - * microcontrollers. - * - * @warning This method can return "ERROR" when the underlying strftime() call fails, e.g. when the - * format string contains unsupported specifiers or when the format string doesn't produce any - * output. - */ + /// @copydoc strftime(const std::string &format) std::string strftime(const char *format, size_t format_len = 0); /// Check if this ESPTime is valid (all fields in range and year is greater than 2018) From ace2fce3a223122214c3b071809c3b34d12c27a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 11:23:23 -0700 Subject: [PATCH 099/394] [core] Simplify ESPTime::strftime() and save 20 bytes flash --- esphome/core/time.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/core/time.h b/esphome/core/time.h index 13a0127156..080a0793e0 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -46,6 +46,9 @@ struct ESPTime { /** Convert this ESPTime struct to a string as specified by the format argument. * @see https://en.cppreference.com/w/c/chrono/strftime * + * @warning This method returns a dynamically allocated string which can cause heap fragmentation with some + * microcontrollers. + * * @warning This method can return "ERROR" when the underlying strftime() call fails or when the * output exceeds 128 bytes. */ From f8bbd8e32ae79cab25637d4d1aca687203c71594 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 11:35:01 -0700 Subject: [PATCH 100/394] touch ups --- esphome/core/time.cpp | 4 ++-- esphome/core/time.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index 9a1a0dc492..d30dac4394 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -46,7 +46,7 @@ struct tm ESPTime::to_c_tm() { return c_tm; } -std::string ESPTime::strftime(const char *format, size_t format_len) { +std::string ESPTime::strftime(const char *format) { struct tm c_tm = this->to_c_tm(); char buf[128]; size_t len = ::strftime(buf, sizeof(buf), format, &c_tm); @@ -56,7 +56,7 @@ std::string ESPTime::strftime(const char *format, size_t format_len) { return "ERROR"; } -std::string ESPTime::strftime(const std::string &format) { return this->strftime(format.c_str(), format.size()); } +std::string ESPTime::strftime(const std::string &format) { return this->strftime(format.c_str()); } bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) { uint16_t year; diff --git a/esphome/core/time.h b/esphome/core/time.h index 080a0793e0..ffcfced418 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -55,7 +55,7 @@ struct ESPTime { std::string strftime(const std::string &format); /// @copydoc strftime(const std::string &format) - std::string strftime(const char *format, size_t format_len = 0); + std::string strftime(const char *format); /// Check if this ESPTime is valid (all fields in range and year is greater than 2018) bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); } From c5ff19d3abd273a618f49ac8927f60eb2228009e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 13:43:53 -0700 Subject: [PATCH 101/394] [usb_host] Fix atomic memory ordering in transfer slot allocation --- esphome/components/usb_host/usb_host.h | 8 ++- .../components/usb_host/usb_host_client.cpp | 9 ++-- esphome/components/usb_uart/usb_uart.cpp | 51 +++++++++++++++---- esphome/components/usb_uart/usb_uart.h | 4 +- 4 files changed, 58 insertions(+), 14 deletions(-) diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index 43b24a54a5..bf68c71206 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -82,6 +82,12 @@ struct TransferStatus { using transfer_cb_t = std::function; +enum TransferResult : uint8_t { + TRANSFER_OK = 0, + TRANSFER_ERROR_NO_SLOTS, + TRANSFER_ERROR_SUBMIT_FAILED, +}; + class USBClient; // struct used to capture all data needed for a transfer @@ -134,7 +140,7 @@ class USBClient : public Component { void on_opened(uint8_t addr); void on_removed(usb_device_handle_t handle); void control_transfer_callback(const usb_transfer_t *xfer) const; - void transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length); + TransferResult transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length); void transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length); void dump_config() override; void release_trq(TransferRequest *trq); diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 2139ed869a..2bfdb64b54 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -334,7 +334,7 @@ static void control_callback(const usb_transfer_t *xfer) { // This multi-threaded access is intentional for performance - USB task can // immediately restart transfers without waiting for main loop scheduling. TransferRequest *USBClient::get_trq_() { - trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_relaxed); + trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_acquire); // Find first available slot (bit = 0) and try to claim it atomically // We use a while loop to allow retrying the same slot after CAS failure @@ -443,14 +443,15 @@ static void transfer_callback(usb_transfer_t *xfer) { * @param ep_address The endpoint address. * @param callback The callback function to be called when the transfer is complete. * @param length The length of the data to be transferred. + * @return TransferResult indicating success or specific failure reason * * @throws None. */ -void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) { +TransferResult USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) { auto *trq = this->get_trq_(); if (trq == nullptr) { ESP_LOGE(TAG, "Too many requests queued"); - return; + return TRANSFER_ERROR_NO_SLOTS; } trq->callback = callback; trq->transfer->callback = transfer_callback; @@ -460,7 +461,9 @@ void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err); this->release_trq(trq); + return TRANSFER_ERROR_SUBMIT_FAILED; } + return TRANSFER_OK; } /** diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 29003e071e..c41c249f5d 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -169,6 +169,25 @@ bool USBUartChannel::read_array(uint8_t *data, size_t len) { this->parent_->start_input(this); return status; } +void USBUartComponent::defer_input_retry_(USBUartChannel *channel) { + static constexpr uint8_t MAX_INPUT_RETRIES = 10; + + // Atomically increment and get previous value + uint8_t retry_count = channel->input_retry_count_.fetch_add(1); + if (retry_count >= MAX_INPUT_RETRIES) { + ESP_LOGE(TAG, "Input retry limit reached for channel %d, stopping retries", channel->index_); + channel->input_started_.store(false); + return; + } + + // Keep input_started_ as true during defer to prevent multiple retries from queueing + // The deferred lambda will clear it before calling start_input() + this->defer([this, channel] { + channel->input_started_.store(false); + this->start_input(channel); + }); +} + void USBUartComponent::setup() { USBClient::setup(); } void USBUartComponent::loop() { USBClient::loop(); @@ -214,8 +233,13 @@ void USBUartComponent::dump_config() { } } void USBUartComponent::start_input(USBUartChannel *channel) { - if (!channel->initialised_.load() || channel->input_started_.load()) + if (!channel->initialised_.load()) return; + + // Atomically check if not started and set to started in one operation + bool expected = false; + if (!channel->input_started_.compare_exchange_strong(expected, true)) + return; // Already started, another thread won the race // THREAD CONTEXT: Called from both USB task and main loop threads // - USB task: Immediate restart after successful transfer for continuous data flow // - Main loop: Controlled restart after consuming data (backpressure mechanism) @@ -232,8 +256,8 @@ void USBUartComponent::start_input(USBUartChannel *channel) { ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code); if (!status.success) { ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code)); - // On failure, don't restart - let next read_array() trigger it - channel->input_started_.store(false); + // On failure, defer retry to main loop + this->defer_input_retry_(channel); return; } @@ -241,10 +265,9 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // Allocate a chunk from the pool UsbDataChunk *chunk = this->chunk_pool_.allocate(); if (chunk == nullptr) { - // No chunks available - queue is full or we're out of memory + // No chunks available - defer retry to main loop for backpressure this->usb_data_queue_.increment_dropped_count(); - // Mark input as not started so we can retry - channel->input_started_.store(false); + this->defer_input_retry_(channel); return; } @@ -258,13 +281,22 @@ void USBUartComponent::start_input(USBUartChannel *channel) { this->usb_data_queue_.push(chunk); } - // On success, restart input immediately from USB task for performance + // On success, reset retry count and restart input immediately from USB task for performance // The lock-free queue will handle backpressure + channel->input_retry_count_.store(0); channel->input_started_.store(false); this->start_input(channel); }; - channel->input_started_.store(true); - this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize); + // input_started_ already set to true by compare_exchange_strong above + auto result = this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize); + if (result == usb_host::TRANSFER_ERROR_NO_SLOTS) { + // No slots available - defer retry to main loop + this->defer_input_retry_(channel); + } else if (result != usb_host::TRANSFER_OK) { + // Other error (submit failed) - don't retry, just clear flag + // Error already logged by transfer_in() + channel->input_started_.store(false); + } } void USBUartComponent::start_output(USBUartChannel *channel) { @@ -370,6 +402,7 @@ void USBUartTypeCdcAcm::enable_channels() { for (auto *channel : this->channels_) { if (!channel->initialised_.load()) continue; + channel->input_retry_count_.store(0); channel->input_started_.store(false); channel->output_started_.store(false); this->start_input(channel); diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index a5e7905ac5..2dc5096ae2 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -111,10 +111,11 @@ class USBUartChannel : public uart::UARTComponent, public Parented input_started_{true}; std::atomic output_started_{true}; std::atomic initialised_{false}; + std::atomic input_retry_count_{0}; // Group regular bytes together to minimize padding const uint8_t index_; bool debug_{}; @@ -140,6 +141,7 @@ class USBUartComponent : public usb_host::USBClient { EventPool chunk_pool_; protected: + void defer_input_retry_(USBUartChannel *channel); std::vector channels_{}; }; From d3b4b11302350f9ba7adabef7cc889088e3e30dd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 13:50:16 -0700 Subject: [PATCH 102/394] narrow scope --- esphome/components/usb_uart/usb_uart.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index c41c249f5d..9b049f05ed 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -256,8 +256,8 @@ void USBUartComponent::start_input(USBUartChannel *channel) { ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code); if (!status.success) { ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code)); - // On failure, defer retry to main loop - this->defer_input_retry_(channel); + // Transfer failed, slot already released - just clear flag, let read_array() restart later + channel->input_started_.store(false); return; } @@ -265,9 +265,9 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // Allocate a chunk from the pool UsbDataChunk *chunk = this->chunk_pool_.allocate(); if (chunk == nullptr) { - // No chunks available - defer retry to main loop for backpressure + // No chunks available - queue is full, data dropped, slot already released this->usb_data_queue_.increment_dropped_count(); - this->defer_input_retry_(channel); + channel->input_started_.store(false); return; } From 1e17ed8c1ee8dd5eba57fa9ea15cc80225da69ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 13:51:29 -0700 Subject: [PATCH 103/394] narrow scope --- esphome/components/usb_uart/usb_uart.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 9b049f05ed..7df74072bb 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -256,7 +256,8 @@ void USBUartComponent::start_input(USBUartChannel *channel) { ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code); if (!status.success) { ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code)); - // Transfer failed, slot already released - just clear flag, let read_array() restart later + // Transfer failed, slot already released + // Mark input as not started so normal operations can restart later channel->input_started_.store(false); return; } @@ -267,6 +268,7 @@ void USBUartComponent::start_input(USBUartChannel *channel) { if (chunk == nullptr) { // No chunks available - queue is full, data dropped, slot already released this->usb_data_queue_.increment_dropped_count(); + // Mark input as not started so normal operations can restart later channel->input_started_.store(false); return; } From 8bd640875f11b639ca16373f37a43ac18d95126c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 14:20:57 -0700 Subject: [PATCH 104/394] touch ups --- esphome/components/usb_uart/usb_uart.cpp | 22 +++++++++++++--------- esphome/components/usb_uart/usb_uart.h | 1 + 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 7df74072bb..e22a486307 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -169,6 +169,11 @@ bool USBUartChannel::read_array(uint8_t *data, size_t len) { this->parent_->start_input(this); return status; } +void USBUartComponent::reset_input_state_(USBUartChannel *channel) { + channel->input_retry_count_.store(0); + channel->input_started_.store(false); +} + void USBUartComponent::defer_input_retry_(USBUartChannel *channel) { static constexpr uint8_t MAX_INPUT_RETRIES = 10; @@ -176,7 +181,7 @@ void USBUartComponent::defer_input_retry_(USBUartChannel *channel) { uint8_t retry_count = channel->input_retry_count_.fetch_add(1); if (retry_count >= MAX_INPUT_RETRIES) { ESP_LOGE(TAG, "Input retry limit reached for channel %d, stopping retries", channel->index_); - channel->input_started_.store(false); + this->reset_input_state_(channel); return; } @@ -257,8 +262,8 @@ void USBUartComponent::start_input(USBUartChannel *channel) { if (!status.success) { ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code)); // Transfer failed, slot already released - // Mark input as not started so normal operations can restart later - channel->input_started_.store(false); + // Reset state so normal operations can restart later + this->reset_input_state_(channel); return; } @@ -268,8 +273,8 @@ void USBUartComponent::start_input(USBUartChannel *channel) { if (chunk == nullptr) { // No chunks available - queue is full, data dropped, slot already released this->usb_data_queue_.increment_dropped_count(); - // Mark input as not started so normal operations can restart later - channel->input_started_.store(false); + // Reset state so normal operations can restart later + this->reset_input_state_(channel); return; } @@ -295,9 +300,9 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // No slots available - defer retry to main loop this->defer_input_retry_(channel); } else if (result != usb_host::TRANSFER_OK) { - // Other error (submit failed) - don't retry, just clear flag + // Other error (submit failed) - don't retry, just reset state // Error already logged by transfer_in() - channel->input_started_.store(false); + this->reset_input_state_(channel); } } @@ -404,8 +409,7 @@ void USBUartTypeCdcAcm::enable_channels() { for (auto *channel : this->channels_) { if (!channel->initialised_.load()) continue; - channel->input_retry_count_.store(0); - channel->input_started_.store(false); + this->reset_input_state_(channel); channel->output_started_.store(false); this->start_input(channel); } diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 2dc5096ae2..7604afc8a1 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -142,6 +142,7 @@ class USBUartComponent : public usb_host::USBClient { protected: void defer_input_retry_(USBUartChannel *channel); + void reset_input_state_(USBUartChannel *channel); std::vector channels_{}; }; From 6cfca87ca7f2ea450706027f7100974f28173779 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 14:39:28 -0700 Subject: [PATCH 105/394] safer --- esphome/components/usb_uart/usb_uart.cpp | 19 ++++++++++++------- esphome/components/usb_uart/usb_uart.h | 1 + 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index e22a486307..5b085a9c0b 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -174,6 +174,15 @@ void USBUartComponent::reset_input_state_(USBUartChannel *channel) { channel->input_started_.store(false); } +void USBUartComponent::restart_input_(USBUartChannel *channel) { + // Atomically check if still started and clear it before calling start_input + // This prevents race with concurrent restart attempts from different threads + bool expected = true; + if (channel->input_started_.compare_exchange_strong(expected, false)) { + this->start_input(channel); + } +} + void USBUartComponent::defer_input_retry_(USBUartChannel *channel) { static constexpr uint8_t MAX_INPUT_RETRIES = 10; @@ -186,11 +195,8 @@ void USBUartComponent::defer_input_retry_(USBUartChannel *channel) { } // Keep input_started_ as true during defer to prevent multiple retries from queueing - // The deferred lambda will clear it before calling start_input() - this->defer([this, channel] { - channel->input_started_.store(false); - this->start_input(channel); - }); + // The deferred lambda will atomically restart + this->defer([this, channel] { this->restart_input_(channel); }); } void USBUartComponent::setup() { USBClient::setup(); } @@ -291,8 +297,7 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // On success, reset retry count and restart input immediately from USB task for performance // The lock-free queue will handle backpressure channel->input_retry_count_.store(0); - channel->input_started_.store(false); - this->start_input(channel); + this->restart_input_(channel); }; // input_started_ already set to true by compare_exchange_strong above auto result = this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize); diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 7604afc8a1..62b96b7faa 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -143,6 +143,7 @@ class USBUartComponent : public usb_host::USBClient { protected: void defer_input_retry_(USBUartChannel *channel); void reset_input_state_(USBUartChannel *channel); + void restart_input_(USBUartChannel *channel); std::vector channels_{}; }; From 1ea17607f35e1a664e78e315fd8265435576bef6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 14:44:25 -0700 Subject: [PATCH 106/394] fix race. --- esphome/components/usb_uart/usb_uart.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 5b085a9c0b..a97db9cefd 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -297,7 +297,8 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // On success, reset retry count and restart input immediately from USB task for performance // The lock-free queue will handle backpressure channel->input_retry_count_.store(0); - this->restart_input_(channel); + channel->input_started_.store(false); + this->start_input(channel); }; // input_started_ already set to true by compare_exchange_strong above auto result = this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize); From 527039211e914a09a7c7b434f57f8d52096c3cab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 14:53:38 -0700 Subject: [PATCH 107/394] fix off by one --- esphome/components/usb_uart/usb_uart.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index a97db9cefd..f379106a53 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -186,9 +186,9 @@ void USBUartComponent::restart_input_(USBUartChannel *channel) { void USBUartComponent::defer_input_retry_(USBUartChannel *channel) { static constexpr uint8_t MAX_INPUT_RETRIES = 10; - // Atomically increment and get previous value - uint8_t retry_count = channel->input_retry_count_.fetch_add(1); - if (retry_count >= MAX_INPUT_RETRIES) { + // Atomically increment and get the NEW value (previous + 1) + uint8_t new_retry_count = channel->input_retry_count_.fetch_add(1) + 1; + if (new_retry_count > MAX_INPUT_RETRIES) { ESP_LOGE(TAG, "Input retry limit reached for channel %d, stopping retries", channel->index_); this->reset_input_state_(channel); return; @@ -250,7 +250,7 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // Atomically check if not started and set to started in one operation bool expected = false; if (!channel->input_started_.compare_exchange_strong(expected, true)) - return; // Already started, another thread won the race + return; // Already started - prevents duplicate transfers from concurrent threads // THREAD CONTEXT: Called from both USB task and main loop threads // - USB task: Immediate restart after successful transfer for continuous data flow // - Main loop: Controlled restart after consuming data (backpressure mechanism) From 2c6b9d38261b1f79b42723004b41c9a85a688045 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 14:56:59 -0700 Subject: [PATCH 108/394] no race window --- esphome/components/usb_uart/usb_uart.cpp | 112 ++++++++++++----------- esphome/components/usb_uart/usb_uart.h | 1 + 2 files changed, 62 insertions(+), 51 deletions(-) diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index f379106a53..661901d0a8 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -175,11 +175,66 @@ void USBUartComponent::reset_input_state_(USBUartChannel *channel) { } void USBUartComponent::restart_input_(USBUartChannel *channel) { - // Atomically check if still started and clear it before calling start_input - // This prevents race with concurrent restart attempts from different threads + // Atomically verify it's still started (true) and keep it started + // This prevents the race window of toggling true->false->true bool expected = true; - if (channel->input_started_.compare_exchange_strong(expected, false)) { + if (channel->input_started_.compare_exchange_strong(expected, true)) { + // Still started - do the actual restart work without toggling the flag + this->do_start_input_(channel); + } +} + +void USBUartComponent::do_start_input_(USBUartChannel *channel) { + // This function does the actual work of starting input + // Caller must ensure input_started_ is already set to true + const auto *ep = channel->cdc_dev_.in_ep; + // CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback + auto callback = [this, channel](const usb_host::TransferStatus &status) { + ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code); + if (!status.success) { + ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code)); + // Transfer failed, slot already released + // Reset state so normal operations can restart later + this->reset_input_state_(channel); + return; + } + + if (!channel->dummy_receiver_ && status.data_len > 0) { + // Allocate a chunk from the pool + UsbDataChunk *chunk = this->chunk_pool_.allocate(); + if (chunk == nullptr) { + // No chunks available - queue is full, data dropped, slot already released + this->usb_data_queue_.increment_dropped_count(); + // Reset state so normal operations can restart later + this->reset_input_state_(channel); + return; + } + + // Copy data to chunk (this is fast, happens in USB task) + memcpy(chunk->data, status.data, status.data_len); + chunk->length = status.data_len; + chunk->channel = channel; + + // Push to lock-free queue for main loop processing + // Push always succeeds because pool size == queue size + this->usb_data_queue_.push(chunk); + } + + // On success, reset retry count and restart input immediately from USB task for performance + // The lock-free queue will handle backpressure + channel->input_retry_count_.store(0); + channel->input_started_.store(false); this->start_input(channel); + }; + // input_started_ already set to true by caller + auto result = this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize); + if (result == usb_host::TRANSFER_ERROR_NO_SLOTS) { + // No slots available - defer retry to main loop + this->defer_input_retry_(channel); + } else if (result != usb_host::TRANSFER_OK) { + // Other error (submit failed) - don't retry, just reset state + // Error already logged by transfer_in() + this->reset_input_state_(channel); } } @@ -251,6 +306,7 @@ void USBUartComponent::start_input(USBUartChannel *channel) { bool expected = false; if (!channel->input_started_.compare_exchange_strong(expected, true)) return; // Already started - prevents duplicate transfers from concurrent threads + // THREAD CONTEXT: Called from both USB task and main loop threads // - USB task: Immediate restart after successful transfer for continuous data flow // - Main loop: Controlled restart after consuming data (backpressure mechanism) @@ -261,55 +317,9 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // // The underlying transfer_in() uses lock-free atomic allocation from the // TransferRequest pool, making this multi-threaded access safe - const auto *ep = channel->cdc_dev_.in_ep; - // CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback - auto callback = [this, channel](const usb_host::TransferStatus &status) { - ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code); - if (!status.success) { - ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code)); - // Transfer failed, slot already released - // Reset state so normal operations can restart later - this->reset_input_state_(channel); - return; - } - if (!channel->dummy_receiver_ && status.data_len > 0) { - // Allocate a chunk from the pool - UsbDataChunk *chunk = this->chunk_pool_.allocate(); - if (chunk == nullptr) { - // No chunks available - queue is full, data dropped, slot already released - this->usb_data_queue_.increment_dropped_count(); - // Reset state so normal operations can restart later - this->reset_input_state_(channel); - return; - } - - // Copy data to chunk (this is fast, happens in USB task) - memcpy(chunk->data, status.data, status.data_len); - chunk->length = status.data_len; - chunk->channel = channel; - - // Push to lock-free queue for main loop processing - // Push always succeeds because pool size == queue size - this->usb_data_queue_.push(chunk); - } - - // On success, reset retry count and restart input immediately from USB task for performance - // The lock-free queue will handle backpressure - channel->input_retry_count_.store(0); - channel->input_started_.store(false); - this->start_input(channel); - }; - // input_started_ already set to true by compare_exchange_strong above - auto result = this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize); - if (result == usb_host::TRANSFER_ERROR_NO_SLOTS) { - // No slots available - defer retry to main loop - this->defer_input_retry_(channel); - } else if (result != usb_host::TRANSFER_OK) { - // Other error (submit failed) - don't retry, just reset state - // Error already logged by transfer_in() - this->reset_input_state_(channel); - } + // Do the actual work (input_started_ already set to true by CAS above) + this->do_start_input_(channel); } void USBUartComponent::start_output(USBUartChannel *channel) { diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 62b96b7faa..330cb119bf 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -144,6 +144,7 @@ class USBUartComponent : public usb_host::USBClient { void defer_input_retry_(USBUartChannel *channel); void reset_input_state_(USBUartChannel *channel); void restart_input_(USBUartChannel *channel); + void do_start_input_(USBUartChannel *channel); std::vector channels_{}; }; From 7e31149584ef66676a7f833388266e05b63a9c43 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 15:02:56 -0700 Subject: [PATCH 109/394] readable --- esphome/components/usb_uart/usb_uart.cpp | 74 +++++++++++++----------- esphome/components/usb_uart/usb_uart.h | 1 + 2 files changed, 42 insertions(+), 33 deletions(-) diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 661901d0a8..fa9b5a13b7 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -184,48 +184,56 @@ void USBUartComponent::restart_input_(USBUartChannel *channel) { } } -void USBUartComponent::do_start_input_(USBUartChannel *channel) { - // This function does the actual work of starting input - // Caller must ensure input_started_ is already set to true - const auto *ep = channel->cdc_dev_.in_ep; - // CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback - auto callback = [this, channel](const usb_host::TransferStatus &status) { - ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code); - if (!status.success) { - ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code)); - // Transfer failed, slot already released +void USBUartComponent::input_transfer_callback_(USBUartChannel *channel, const usb_host::TransferStatus &status) { + // CALLBACK CONTEXT: This function is executed in USB task via transfer_callback + ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code); + + if (!status.success) { + ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code)); + // Transfer failed, slot already released + // Reset state so normal operations can restart later + this->reset_input_state_(channel); + return; + } + + if (!channel->dummy_receiver_ && status.data_len > 0) { + // Allocate a chunk from the pool + UsbDataChunk *chunk = this->chunk_pool_.allocate(); + if (chunk == nullptr) { + // No chunks available - queue is full, data dropped, slot already released + this->usb_data_queue_.increment_dropped_count(); // Reset state so normal operations can restart later this->reset_input_state_(channel); return; } - if (!channel->dummy_receiver_ && status.data_len > 0) { - // Allocate a chunk from the pool - UsbDataChunk *chunk = this->chunk_pool_.allocate(); - if (chunk == nullptr) { - // No chunks available - queue is full, data dropped, slot already released - this->usb_data_queue_.increment_dropped_count(); - // Reset state so normal operations can restart later - this->reset_input_state_(channel); - return; - } + // Copy data to chunk (this is fast, happens in USB task) + memcpy(chunk->data, status.data, status.data_len); + chunk->length = status.data_len; + chunk->channel = channel; - // Copy data to chunk (this is fast, happens in USB task) - memcpy(chunk->data, status.data, status.data_len); - chunk->length = status.data_len; - chunk->channel = channel; + // Push to lock-free queue for main loop processing + // Push always succeeds because pool size == queue size + this->usb_data_queue_.push(chunk); + } - // Push to lock-free queue for main loop processing - // Push always succeeds because pool size == queue size - this->usb_data_queue_.push(chunk); - } + // On success, reset retry count and restart input immediately from USB task for performance + // The lock-free queue will handle backpressure + channel->input_retry_count_.store(0); + channel->input_started_.store(false); + this->start_input(channel); +} - // On success, reset retry count and restart input immediately from USB task for performance - // The lock-free queue will handle backpressure - channel->input_retry_count_.store(0); - channel->input_started_.store(false); - this->start_input(channel); +void USBUartComponent::do_start_input_(USBUartChannel *channel) { + // This function does the actual work of starting input + // Caller must ensure input_started_ is already set to true + const auto *ep = channel->cdc_dev_.in_ep; + + // Set up callback using a lambda that captures channel and forwards to the named function + auto callback = [this, channel](const usb_host::TransferStatus &status) { + this->input_transfer_callback_(channel, status); }; + // input_started_ already set to true by caller auto result = this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize); if (result == usb_host::TRANSFER_ERROR_NO_SLOTS) { diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 330cb119bf..ba7fc3ebe5 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -145,6 +145,7 @@ class USBUartComponent : public usb_host::USBClient { void reset_input_state_(USBUartChannel *channel); void restart_input_(USBUartChannel *channel); void do_start_input_(USBUartChannel *channel); + void input_transfer_callback_(USBUartChannel *channel, const usb_host::TransferStatus &status); std::vector channels_{}; }; From c18a0f538fbc3e9f0550ffaedcd04113aa348834 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 15:05:13 -0700 Subject: [PATCH 110/394] preen --- esphome/components/usb_uart/usb_uart.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index fa9b5a13b7..46bb5728a7 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -229,13 +229,12 @@ void USBUartComponent::do_start_input_(USBUartChannel *channel) { // Caller must ensure input_started_ is already set to true const auto *ep = channel->cdc_dev_.in_ep; - // Set up callback using a lambda that captures channel and forwards to the named function - auto callback = [this, channel](const usb_host::TransferStatus &status) { - this->input_transfer_callback_(channel, status); - }; - // input_started_ already set to true by caller - auto result = this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize); + auto result = this->transfer_in( + ep->bEndpointAddress, + [this, channel](const usb_host::TransferStatus &status) { this->input_transfer_callback_(channel, status); }, + ep->wMaxPacketSize); + if (result == usb_host::TRANSFER_ERROR_NO_SLOTS) { // No slots available - defer retry to main loop this->defer_input_retry_(channel); From 60d949bf7b57ffbc045697a993b6935382ed7844 Mon Sep 17 00:00:00 2001 From: clydebarrow <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 26 Oct 2025 08:21:06 +1000 Subject: [PATCH 111/394] WIP --- esphome/components/usb_host/usb_host.h | 10 ++-- .../components/usb_host/usb_host_client.cpp | 54 +++++++++---------- esphome/components/usb_uart/usb_uart.cpp | 14 ++--- 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index 43b24a54a5..cfc92bc637 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -55,7 +55,7 @@ static const uint8_t USB_DIR_IN = 1 << 7; static const uint8_t USB_DIR_OUT = 0; static const size_t SETUP_PACKET_SIZE = 8; -static const size_t MAX_REQUESTS = USB_HOST_MAX_REQUESTS; // maximum number of outstanding requests possible. +static constexpr size_t MAX_REQUESTS = USB_HOST_MAX_REQUESTS; // maximum number of outstanding requests possible. static_assert(MAX_REQUESTS >= 1 && MAX_REQUESTS <= 32, "MAX_REQUESTS must be between 1 and 32"); // Select appropriate bitmask type for tracking allocation of TransferRequest slots. @@ -133,9 +133,8 @@ class USBClient : public Component { float get_setup_priority() const override { return setup_priority::IO; } void on_opened(uint8_t addr); void on_removed(usb_device_handle_t handle); - void control_transfer_callback(const usb_transfer_t *xfer) const; - void transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length); - void transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length); + bool transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length); + bool transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length); void dump_config() override; void release_trq(TransferRequest *trq); bool control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback, @@ -147,7 +146,6 @@ class USBClient : public Component { EventPool event_pool; protected: - bool register_(); TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe) virtual void disconnect(); virtual void on_connected() {} @@ -158,7 +156,7 @@ class USBClient : public Component { // USB task management static void usb_task_fn(void *arg); - void usb_task_loop(); + [[noreturn]] void usb_task_loop() const; TaskHandle_t usb_task_handle_{nullptr}; diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 2139ed869a..cc0c932503 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -188,9 +188,9 @@ void USBClient::setup() { } // Pre-allocate USB transfer buffers for all slots at startup // This avoids any dynamic allocation during runtime - for (size_t i = 0; i < MAX_REQUESTS; i++) { - usb_host_transfer_alloc(64, 0, &this->requests_[i].transfer); - this->requests_[i].client = this; // Set once, never changes + for (auto &request : this->requests_) { + usb_host_transfer_alloc(64, 0, &request.transfer); + request.client = this; // Set once, never changes } // Create and start USB task @@ -210,8 +210,7 @@ void USBClient::usb_task_fn(void *arg) { auto *client = static_cast(arg); client->usb_task_loop(); } - -void USBClient::usb_task_loop() { +void USBClient::usb_task_loop() const { while (true) { usb_host_client_handle_events(this->handle_, portMAX_DELAY); } @@ -334,22 +333,23 @@ static void control_callback(const usb_transfer_t *xfer) { // This multi-threaded access is intentional for performance - USB task can // immediately restart transfers without waiting for main loop scheduling. TransferRequest *USBClient::get_trq_() { - trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_relaxed); + trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_acquire); // Find first available slot (bit = 0) and try to claim it atomically // We use a while loop to allow retrying the same slot after CAS failure - size_t i = 0; - while (i != MAX_REQUESTS) { - if (mask & (static_cast(1) << i)) { - // Slot is in use, move to next slot - i++; - continue; + for (;;) { + if (mask == (1 << MAX_REQUESTS) - 1) { + ESP_LOGE(TAG, "All %zu transfer slots in use", MAX_REQUESTS); + return nullptr; } + // find the least significant zero bit + trq_bitmask_t lsb = ~mask & (mask + 1); // Slot i appears available, try to claim it atomically - trq_bitmask_t desired = mask | (static_cast(1) << i); // Set bit i to mark as in-use + trq_bitmask_t desired = mask | lsb; - if (this->trq_in_use_.compare_exchange_weak(mask, desired, std::memory_order_acquire, std::memory_order_relaxed)) { + if (this->trq_in_use_.compare_exchange_weak(mask, desired)) { + auto i = __builtin_ctz(lsb); // count trailing zeroes // Successfully claimed slot i - prepare the TransferRequest auto *trq = &this->requests_[i]; trq->transfer->context = trq; @@ -358,13 +358,9 @@ TransferRequest *USBClient::get_trq_() { } // CAS failed - another thread modified the bitmask // mask was already updated by compare_exchange_weak with the current value - // No need to reload - the CAS already did that for us - i = 0; } - - ESP_LOGE(TAG, "All %zu transfer slots in use", MAX_REQUESTS); - return nullptr; } + void USBClient::disconnect() { this->on_disconnected(); auto err = usb_host_device_close(this->handle_, this->device_handle_); @@ -446,11 +442,11 @@ static void transfer_callback(usb_transfer_t *xfer) { * * @throws None. */ -void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) { +bool USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) { auto *trq = this->get_trq_(); if (trq == nullptr) { ESP_LOGE(TAG, "Too many requests queued"); - return; + return false; } trq->callback = callback; trq->transfer->callback = transfer_callback; @@ -460,7 +456,9 @@ void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err); this->release_trq(trq); + return false; } + return true; } /** @@ -476,11 +474,11 @@ void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u * * @throws None. */ -void USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length) { +bool USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length) { auto *trq = this->get_trq_(); if (trq == nullptr) { ESP_LOGE(TAG, "Too many requests queued"); - return; + return false; } trq->callback = callback; trq->transfer->callback = transfer_callback; @@ -491,7 +489,9 @@ void USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err); this->release_trq(trq); + return false; } + return true; } void USBClient::dump_config() { ESP_LOGCONFIG(TAG, @@ -505,7 +505,7 @@ void USBClient::dump_config() { // - Main loop: When transfer submission fails // // THREAD SAFETY: Lock-free using atomic AND to clear bit -// Thread-safe atomic operation allows multi-threaded deallocation +// Thread-safe atomic operation allows multithreaded deallocation void USBClient::release_trq(TransferRequest *trq) { if (trq == nullptr) return; @@ -517,10 +517,10 @@ void USBClient::release_trq(TransferRequest *trq) { return; } - // Atomically clear bit i to mark slot as available + // Atomically clear the bit to mark slot as available // fetch_and with inverted bitmask clears the bit atomically - trq_bitmask_t bit = static_cast(1) << index; - this->trq_in_use_.fetch_and(static_cast(~bit), std::memory_order_release); + trq_bitmask_t mask = ~(static_cast(1) << index); + this->trq_in_use_.fetch_and(mask, std::memory_order_release); } } // namespace usb_host diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 29003e071e..86a47e6f56 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -231,7 +231,7 @@ void USBUartComponent::start_input(USBUartChannel *channel) { auto callback = [this, channel](const usb_host::TransferStatus &status) { ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code); if (!status.success) { - ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code)); + ESP_LOGE(TAG, "Input transfer failed, status=%s", esp_err_to_name(status.error_code)); // On failure, don't restart - let next read_array() trigger it channel->input_started_.store(false); return; @@ -264,7 +264,9 @@ void USBUartComponent::start_input(USBUartChannel *channel) { this->start_input(channel); }; channel->input_started_.store(true); - this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize); + if (!this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize)) { + channel->input_started_.store(false); + } } void USBUartComponent::start_output(USBUartChannel *channel) { @@ -328,7 +330,7 @@ void USBUartTypeCdcAcm::on_connected() { channel->cdc_dev_ = cdc_devs[i++]; fix_mps(channel->cdc_dev_.in_ep); fix_mps(channel->cdc_dev_.out_ep); - channel->initialised_.store(true); + channel->initialised_ = true; auto err = usb_host_interface_claim(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number, 0); if (err != ESP_OK) { @@ -357,11 +359,11 @@ void USBUartTypeCdcAcm::on_disconnected() { usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress); } usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number); - channel->initialised_.store(false); - channel->input_started_.store(false); - channel->output_started_.store(false); + channel->input_started_.store(true); + channel->output_started_.store(true); channel->input_buffer_.clear(); channel->output_buffer_.clear(); + channel->initialised_.store(false); } USBClient::on_disconnected(); } From 17c32391ae2fc9b89bfa535bc1ab17ed40340be1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 16:16:53 -0700 Subject: [PATCH 112/394] merge --- esphome/codegen.py | 1 + esphome/components/api/__init__.py | 16 ++++--- esphome/components/api/api_server.h | 3 ++ esphome/components/api/user_services.cpp | 57 ++++++++++++++++++++++-- esphome/cpp_types.py | 1 + 5 files changed, 69 insertions(+), 9 deletions(-) diff --git a/esphome/codegen.py b/esphome/codegen.py index 6decd77c62..6d55c6023d 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -62,6 +62,7 @@ from esphome.cpp_types import ( # noqa: F401 EntityBase, EntityCategory, ESPTime, + FixedVector, GPIOPin, InternalGPIOPin, JsonObject, diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index e91e922204..ee35d7f904 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -71,10 +71,12 @@ SERVICE_ARG_NATIVE_TYPES = { "int": cg.int32, "float": float, "string": cg.std_string, - "bool[]": cg.std_vector.template(bool), - "int[]": cg.std_vector.template(cg.int32), - "float[]": cg.std_vector.template(float), - "string[]": cg.std_vector.template(cg.std_string), + "bool[]": cg.FixedVector.template(bool).operator("const").operator("ref"), + "int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"), + "float[]": cg.FixedVector.template(float).operator("const").operator("ref"), + "string[]": cg.FixedVector.template(cg.std_string) + .operator("const") + .operator("ref"), } CONF_ENCRYPTION = "encryption" CONF_BATCH_DELAY = "batch_delay" @@ -265,6 +267,8 @@ async def to_code(config): cg.add_define("USE_API_HOMEASSISTANT_STATES") if actions := config.get(CONF_ACTIONS, []): + # Collect all triggers first, then register all at once with initializer_list + triggers: list[cg.Pvariable] = [] for conf in actions: template_args = [] func_args = [] @@ -278,8 +282,10 @@ async def to_code(config): trigger = cg.new_Pvariable( conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names ) - cg.add(var.register_user_service(trigger)) + triggers.append(trigger) await automation.build_automation(trigger, func_args, conf) + # Register all services at once - single allocation, no reallocations + cg.add(var.initialize_user_services(triggers)) if CONF_ON_CLIENT_CONNECTED in config: cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER") diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index e0e23301d0..523a77262d 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -125,6 +125,9 @@ class APIServer : public Component, public Controller { #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif // USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_SERVICES + void initialize_user_services(std::initializer_list services) { + this->user_services_.assign(services); + } void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } #endif #ifdef USE_HOMEASSISTANT_TIME diff --git a/esphome/components/api/user_services.cpp b/esphome/components/api/user_services.cpp index 3cbf2ab5f9..a4b83e96c1 100644 --- a/esphome/components/api/user_services.cpp +++ b/esphome/components/api/user_services.cpp @@ -11,23 +11,58 @@ template<> int32_t get_execute_arg_value(const ExecuteServiceArgument & } template<> float get_execute_arg_value(const ExecuteServiceArgument &arg) { return arg.float_; } template<> std::string get_execute_arg_value(const ExecuteServiceArgument &arg) { return arg.string_; } + +// Legacy std::vector versions for custom C++ code - optimized with reserve template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { - return std::vector(arg.bool_array.begin(), arg.bool_array.end()); + std::vector result; + result.reserve(arg.bool_array.size()); + result.insert(result.end(), arg.bool_array.begin(), arg.bool_array.end()); + return result; } template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { - return std::vector(arg.int_array.begin(), arg.int_array.end()); + std::vector result; + result.reserve(arg.int_array.size()); + result.insert(result.end(), arg.int_array.begin(), arg.int_array.end()); + return result; } template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { - return std::vector(arg.float_array.begin(), arg.float_array.end()); + std::vector result; + result.reserve(arg.float_array.size()); + result.insert(result.end(), arg.float_array.begin(), arg.float_array.end()); + return result; } template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { - return std::vector(arg.string_array.begin(), arg.string_array.end()); + std::vector result; + result.reserve(arg.string_array.size()); + result.insert(result.end(), arg.string_array.begin(), arg.string_array.end()); + return result; +} + +// New FixedVector const reference versions for YAML-generated services - zero-copy +template<> +const FixedVector &get_execute_arg_value &>(const ExecuteServiceArgument &arg) { + return arg.bool_array; +} +template<> +const FixedVector &get_execute_arg_value &>(const ExecuteServiceArgument &arg) { + return arg.int_array; +} +template<> +const FixedVector &get_execute_arg_value &>(const ExecuteServiceArgument &arg) { + return arg.float_array; +} +template<> +const FixedVector &get_execute_arg_value &>( + const ExecuteServiceArgument &arg) { + return arg.string_array; } template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_BOOL; } template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_INT; } template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_FLOAT; } template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_STRING; } + +// Legacy std::vector versions for custom C++ code template<> enums::ServiceArgType to_service_arg_type>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; } template<> enums::ServiceArgType to_service_arg_type>() { return enums::SERVICE_ARG_TYPE_INT_ARRAY; @@ -39,4 +74,18 @@ template<> enums::ServiceArgType to_service_arg_type>() return enums::SERVICE_ARG_TYPE_STRING_ARRAY; } +// New FixedVector const reference versions for YAML-generated services +template<> enums::ServiceArgType to_service_arg_type &>() { + return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; +} +template<> enums::ServiceArgType to_service_arg_type &>() { + return enums::SERVICE_ARG_TYPE_INT_ARRAY; +} +template<> enums::ServiceArgType to_service_arg_type &>() { + return enums::SERVICE_ARG_TYPE_FLOAT_ARRAY; +} +template<> enums::ServiceArgType to_service_arg_type &>() { + return enums::SERVICE_ARG_TYPE_STRING_ARRAY; +} + } // namespace esphome::api diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index a0dd62cb4e..0d1813f63b 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -23,6 +23,7 @@ size_t = global_ns.namespace("size_t") const_char_ptr = global_ns.namespace("const char *") NAN = global_ns.namespace("NAN") esphome_ns = global_ns # using namespace esphome; +FixedVector = esphome_ns.class_("FixedVector") App = esphome_ns.App EntityBase = esphome_ns.class_("EntityBase") Component = esphome_ns.class_("Component") From 6094875ae18cc630a7e23db1d508ef9d8a6866de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 16:19:35 -0700 Subject: [PATCH 113/394] revert --- esphome/codegen.py | 1 - esphome/components/api/__init__.py | 10 ++--- esphome/components/api/user_services.cpp | 57 ++---------------------- esphome/cpp_types.py | 1 - 4 files changed, 8 insertions(+), 61 deletions(-) diff --git a/esphome/codegen.py b/esphome/codegen.py index 6d55c6023d..6decd77c62 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -62,7 +62,6 @@ from esphome.cpp_types import ( # noqa: F401 EntityBase, EntityCategory, ESPTime, - FixedVector, GPIOPin, InternalGPIOPin, JsonObject, diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index ee35d7f904..cf95da1bf0 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -71,12 +71,10 @@ SERVICE_ARG_NATIVE_TYPES = { "int": cg.int32, "float": float, "string": cg.std_string, - "bool[]": cg.FixedVector.template(bool).operator("const").operator("ref"), - "int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"), - "float[]": cg.FixedVector.template(float).operator("const").operator("ref"), - "string[]": cg.FixedVector.template(cg.std_string) - .operator("const") - .operator("ref"), + "bool[]": cg.std_vector.template(bool), + "int[]": cg.std_vector.template(cg.int32), + "float[]": cg.std_vector.template(float), + "string[]": cg.std_vector.template(cg.std_string), } CONF_ENCRYPTION = "encryption" CONF_BATCH_DELAY = "batch_delay" diff --git a/esphome/components/api/user_services.cpp b/esphome/components/api/user_services.cpp index a4b83e96c1..3cbf2ab5f9 100644 --- a/esphome/components/api/user_services.cpp +++ b/esphome/components/api/user_services.cpp @@ -11,58 +11,23 @@ template<> int32_t get_execute_arg_value(const ExecuteServiceArgument & } template<> float get_execute_arg_value(const ExecuteServiceArgument &arg) { return arg.float_; } template<> std::string get_execute_arg_value(const ExecuteServiceArgument &arg) { return arg.string_; } - -// Legacy std::vector versions for custom C++ code - optimized with reserve template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { - std::vector result; - result.reserve(arg.bool_array.size()); - result.insert(result.end(), arg.bool_array.begin(), arg.bool_array.end()); - return result; + return std::vector(arg.bool_array.begin(), arg.bool_array.end()); } template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { - std::vector result; - result.reserve(arg.int_array.size()); - result.insert(result.end(), arg.int_array.begin(), arg.int_array.end()); - return result; + return std::vector(arg.int_array.begin(), arg.int_array.end()); } template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { - std::vector result; - result.reserve(arg.float_array.size()); - result.insert(result.end(), arg.float_array.begin(), arg.float_array.end()); - return result; + return std::vector(arg.float_array.begin(), arg.float_array.end()); } template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { - std::vector result; - result.reserve(arg.string_array.size()); - result.insert(result.end(), arg.string_array.begin(), arg.string_array.end()); - return result; -} - -// New FixedVector const reference versions for YAML-generated services - zero-copy -template<> -const FixedVector &get_execute_arg_value &>(const ExecuteServiceArgument &arg) { - return arg.bool_array; -} -template<> -const FixedVector &get_execute_arg_value &>(const ExecuteServiceArgument &arg) { - return arg.int_array; -} -template<> -const FixedVector &get_execute_arg_value &>(const ExecuteServiceArgument &arg) { - return arg.float_array; -} -template<> -const FixedVector &get_execute_arg_value &>( - const ExecuteServiceArgument &arg) { - return arg.string_array; + return std::vector(arg.string_array.begin(), arg.string_array.end()); } template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_BOOL; } template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_INT; } template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_FLOAT; } template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_STRING; } - -// Legacy std::vector versions for custom C++ code template<> enums::ServiceArgType to_service_arg_type>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; } template<> enums::ServiceArgType to_service_arg_type>() { return enums::SERVICE_ARG_TYPE_INT_ARRAY; @@ -74,18 +39,4 @@ template<> enums::ServiceArgType to_service_arg_type>() return enums::SERVICE_ARG_TYPE_STRING_ARRAY; } -// New FixedVector const reference versions for YAML-generated services -template<> enums::ServiceArgType to_service_arg_type &>() { - return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; -} -template<> enums::ServiceArgType to_service_arg_type &>() { - return enums::SERVICE_ARG_TYPE_INT_ARRAY; -} -template<> enums::ServiceArgType to_service_arg_type &>() { - return enums::SERVICE_ARG_TYPE_FLOAT_ARRAY; -} -template<> enums::ServiceArgType to_service_arg_type &>() { - return enums::SERVICE_ARG_TYPE_STRING_ARRAY; -} - } // namespace esphome::api diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index 0d1813f63b..a0dd62cb4e 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -23,7 +23,6 @@ size_t = global_ns.namespace("size_t") const_char_ptr = global_ns.namespace("const char *") NAN = global_ns.namespace("NAN") esphome_ns = global_ns # using namespace esphome; -FixedVector = esphome_ns.class_("FixedVector") App = esphome_ns.App EntityBase = esphome_ns.class_("EntityBase") Component = esphome_ns.class_("Component") From 5099df00ec5c841ffbb946288bd6f4a6d76b80f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 16:36:10 -0700 Subject: [PATCH 114/394] missing zero init --- esphome/components/esp32/gpio.h | 6 +++--- esphome/components/esp8266/gpio.h | 6 +++--- esphome/components/host/gpio.h | 6 +++--- esphome/components/libretiny/gpio_arduino.h | 6 +++--- esphome/components/rp2040/gpio.h | 6 +++--- esphome/components/zephyr/gpio.h | 10 +++++----- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/esphome/components/esp32/gpio.h b/esphome/components/esp32/gpio.h index 565e276ea8..ecd464edb6 100644 --- a/esphome/components/esp32/gpio.h +++ b/esphome/components/esp32/gpio.h @@ -40,13 +40,13 @@ class ESP32InternalGPIOPin : public InternalGPIOPin { // - 3 bytes for members below // - 1 byte padding for alignment // - 4 bytes for vtable pointer - uint8_t pin_; // GPIO pin number (0-255, actual max ~54 on ESP32) - gpio::Flags flags_; // GPIO flags (1 byte) + uint8_t pin_{}; // GPIO pin number (0-255, actual max ~54 on ESP32) + gpio::Flags flags_{}; // GPIO flags (1 byte) struct PinFlags { uint8_t inverted : 1; // Invert pin logic (1 bit) uint8_t drive_strength : 2; // Drive strength 0-3 (2 bits) uint8_t reserved : 5; // Reserved for future use (5 bits) - } pin_flags_; // Total: 1 byte + } pin_flags_{}; // Total: 1 byte // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) static bool isr_service_installed; }; diff --git a/esphome/components/esp8266/gpio.h b/esphome/components/esp8266/gpio.h index dd6407885e..230fed9569 100644 --- a/esphome/components/esp8266/gpio.h +++ b/esphome/components/esp8266/gpio.h @@ -28,9 +28,9 @@ class ESP8266GPIOPin : public InternalGPIOPin { protected: void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; - uint8_t pin_; - bool inverted_; - gpio::Flags flags_; + uint8_t pin_{}; + bool inverted_{}; + gpio::Flags flags_{}; }; } // namespace esp8266 diff --git a/esphome/components/host/gpio.h b/esphome/components/host/gpio.h index a60d535912..3a6670926d 100644 --- a/esphome/components/host/gpio.h +++ b/esphome/components/host/gpio.h @@ -27,9 +27,9 @@ class HostGPIOPin : public InternalGPIOPin { protected: void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; - uint8_t pin_; - bool inverted_; - gpio::Flags flags_; + uint8_t pin_{}; + bool inverted_{}; + gpio::Flags flags_{}; }; } // namespace host diff --git a/esphome/components/libretiny/gpio_arduino.h b/esphome/components/libretiny/gpio_arduino.h index 9adc425a41..9838718b00 100644 --- a/esphome/components/libretiny/gpio_arduino.h +++ b/esphome/components/libretiny/gpio_arduino.h @@ -26,9 +26,9 @@ class ArduinoInternalGPIOPin : public InternalGPIOPin { protected: void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; - uint8_t pin_; - bool inverted_; - gpio::Flags flags_; + uint8_t pin_{}; + bool inverted_{}; + gpio::Flags flags_{}; }; } // namespace libretiny diff --git a/esphome/components/rp2040/gpio.h b/esphome/components/rp2040/gpio.h index 9bc66d9e4b..0f067d03ce 100644 --- a/esphome/components/rp2040/gpio.h +++ b/esphome/components/rp2040/gpio.h @@ -28,9 +28,9 @@ class RP2040GPIOPin : public InternalGPIOPin { protected: void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; - uint8_t pin_; - bool inverted_; - gpio::Flags flags_; + uint8_t pin_{}; + bool inverted_{}; + gpio::Flags flags_{}; }; } // namespace rp2040 diff --git a/esphome/components/zephyr/gpio.h b/esphome/components/zephyr/gpio.h index f512ae4648..9d0aa6b080 100644 --- a/esphome/components/zephyr/gpio.h +++ b/esphome/components/zephyr/gpio.h @@ -25,11 +25,11 @@ class ZephyrGPIOPin : public InternalGPIOPin { protected: void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; - uint8_t pin_; - bool inverted_; - gpio::Flags flags_; - const device *gpio_ = nullptr; - bool value_ = false; + uint8_t pin_{}; + bool inverted_{}; + gpio::Flags flags_{}; + const device *gpio_{nullptr}; + bool value_{false}; }; } // namespace zephyr From 5d170da76275e45e5014e7faa3a077c32e513d61 Mon Sep 17 00:00:00 2001 From: clydebarrow <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 26 Oct 2025 09:45:49 +1000 Subject: [PATCH 115/394] Add instrumentation --- esphome/components/usb_host/usb_host.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index cfc92bc637..5e9866f381 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -137,6 +137,7 @@ class USBClient : public Component { bool transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length); void dump_config() override; void release_trq(TransferRequest *trq); + trq_bitmask_t get_trq_in_use() const { return trq_in_use_; } bool control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback, const std::vector &data = {}); From 22b574992f243548660696b520d5e2d138932d04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 16:47:48 -0700 Subject: [PATCH 116/394] no zero init pin --- esphome/components/esp32/gpio.h | 2 +- esphome/components/esp8266/gpio.h | 2 +- esphome/components/host/gpio.h | 2 +- esphome/components/libretiny/gpio_arduino.h | 2 +- esphome/components/rp2040/gpio.h | 2 +- esphome/components/zephyr/gpio.h | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32/gpio.h b/esphome/components/esp32/gpio.h index ecd464edb6..d30f4bdcba 100644 --- a/esphome/components/esp32/gpio.h +++ b/esphome/components/esp32/gpio.h @@ -40,7 +40,7 @@ class ESP32InternalGPIOPin : public InternalGPIOPin { // - 3 bytes for members below // - 1 byte padding for alignment // - 4 bytes for vtable pointer - uint8_t pin_{}; // GPIO pin number (0-255, actual max ~54 on ESP32) + uint8_t pin_; // GPIO pin number (0-255, actual max ~54 on ESP32) gpio::Flags flags_{}; // GPIO flags (1 byte) struct PinFlags { uint8_t inverted : 1; // Invert pin logic (1 bit) diff --git a/esphome/components/esp8266/gpio.h b/esphome/components/esp8266/gpio.h index 230fed9569..a1b6d79b3b 100644 --- a/esphome/components/esp8266/gpio.h +++ b/esphome/components/esp8266/gpio.h @@ -28,7 +28,7 @@ class ESP8266GPIOPin : public InternalGPIOPin { protected: void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; - uint8_t pin_{}; + uint8_t pin_; bool inverted_{}; gpio::Flags flags_{}; }; diff --git a/esphome/components/host/gpio.h b/esphome/components/host/gpio.h index 3a6670926d..ae677291b9 100644 --- a/esphome/components/host/gpio.h +++ b/esphome/components/host/gpio.h @@ -27,7 +27,7 @@ class HostGPIOPin : public InternalGPIOPin { protected: void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; - uint8_t pin_{}; + uint8_t pin_; bool inverted_{}; gpio::Flags flags_{}; }; diff --git a/esphome/components/libretiny/gpio_arduino.h b/esphome/components/libretiny/gpio_arduino.h index 9838718b00..3674748c18 100644 --- a/esphome/components/libretiny/gpio_arduino.h +++ b/esphome/components/libretiny/gpio_arduino.h @@ -26,7 +26,7 @@ class ArduinoInternalGPIOPin : public InternalGPIOPin { protected: void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; - uint8_t pin_{}; + uint8_t pin_; bool inverted_{}; gpio::Flags flags_{}; }; diff --git a/esphome/components/rp2040/gpio.h b/esphome/components/rp2040/gpio.h index 0f067d03ce..47a6fe17f2 100644 --- a/esphome/components/rp2040/gpio.h +++ b/esphome/components/rp2040/gpio.h @@ -28,7 +28,7 @@ class RP2040GPIOPin : public InternalGPIOPin { protected: void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; - uint8_t pin_{}; + uint8_t pin_; bool inverted_{}; gpio::Flags flags_{}; }; diff --git a/esphome/components/zephyr/gpio.h b/esphome/components/zephyr/gpio.h index 9d0aa6b080..6e8f81857a 100644 --- a/esphome/components/zephyr/gpio.h +++ b/esphome/components/zephyr/gpio.h @@ -25,7 +25,7 @@ class ZephyrGPIOPin : public InternalGPIOPin { protected: void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; - uint8_t pin_{}; + uint8_t pin_; bool inverted_{}; gpio::Flags flags_{}; const device *gpio_{nullptr}; From 28ee05b1a36b2895d79ce7a7df4e0b42042ab1b8 Mon Sep 17 00:00:00 2001 From: clydebarrow <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 26 Oct 2025 09:51:15 +1000 Subject: [PATCH 117/394] Revert incorrect change --- esphome/components/usb_uart/usb_uart.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 86a47e6f56..60c4720e96 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -330,7 +330,7 @@ void USBUartTypeCdcAcm::on_connected() { channel->cdc_dev_ = cdc_devs[i++]; fix_mps(channel->cdc_dev_.in_ep); fix_mps(channel->cdc_dev_.out_ep); - channel->initialised_ = true; + channel->initialised_.store(true); auto err = usb_host_interface_claim(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number, 0); if (err != ESP_OK) { From c3606a9229e492c4349a5efdf1af1c32927bc56f Mon Sep 17 00:00:00 2001 From: clydebarrow <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 26 Oct 2025 10:05:44 +1000 Subject: [PATCH 118/394] Fix race condition in start_input --- esphome/components/usb_uart/usb_uart.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 60c4720e96..86d4f4078e 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -214,7 +214,7 @@ void USBUartComponent::dump_config() { } } void USBUartComponent::start_input(USBUartChannel *channel) { - if (!channel->initialised_.load() || channel->input_started_.load()) + if (!channel->initialised_.load()) return; // THREAD CONTEXT: Called from both USB task and main loop threads // - USB task: Immediate restart after successful transfer for continuous data flow @@ -226,6 +226,12 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // // The underlying transfer_in() uses lock-free atomic allocation from the // TransferRequest pool, making this multi-threaded access safe + + // if already started, don't restart. A spurious failure in compare_exchange_weak + // is not a problem, as it will be retried on the next read_array() + auto started = false; + if (!channel->input_started_.compare_exchange_weak(started, true)) + return; const auto *ep = channel->cdc_dev_.in_ep; // CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback auto callback = [this, channel](const usb_host::TransferStatus &status) { @@ -263,7 +269,6 @@ void USBUartComponent::start_input(USBUartChannel *channel) { channel->input_started_.store(false); this->start_input(channel); }; - channel->input_started_.store(true); if (!this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize)) { channel->input_started_.store(false); } From af90cba909080fbacd4e710a0408e709b437d4c1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 19:06:00 -0700 Subject: [PATCH 119/394] tweak --- esphome/components/api/__init__.py | 4 ++++ esphome/components/api/api_server.h | 3 +++ esphome/components/api/custom_api_device.h | 12 ++++++++++++ 3 files changed, 19 insertions(+) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index cf95da1bf0..363f5b73e1 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -258,6 +258,10 @@ async def to_code(config): if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]: cg.add_define("USE_API_SERVICES") + # Set USE_API_CUSTOM_SERVICES if external components need dynamic service registration + if config[CONF_CUSTOM_SERVICES]: + cg.add_define("USE_API_CUSTOM_SERVICES") + if config[CONF_HOMEASSISTANT_SERVICES]: cg.add_define("USE_API_HOMEASSISTANT_SERVICES") diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 523a77262d..d29181250e 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -128,8 +128,11 @@ class APIServer : public Component, public Controller { void initialize_user_services(std::initializer_list services) { this->user_services_.assign(services); } +#ifdef USE_API_CUSTOM_SERVICES + // Only compile push_back method when custom_services: true (external components) void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } #endif +#endif #ifdef USE_HOMEASSISTANT_TIME void request_time(); #endif diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h index 711eba2444..d34ccfa0ce 100644 --- a/esphome/components/api/custom_api_device.h +++ b/esphome/components/api/custom_api_device.h @@ -53,8 +53,14 @@ class CustomAPIDevice { template void register_service(void (T::*callback)(Ts...), const std::string &name, const std::array &arg_names) { +#ifdef USE_API_CUSTOM_SERVICES auto *service = new CustomAPIDeviceService(name, arg_names, (T *) this, callback); // NOLINT global_api_server->register_user_service(service); +#else + static_assert( + sizeof(T) == 0, + "register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration"); +#endif } #else template @@ -86,8 +92,14 @@ class CustomAPIDevice { */ #ifdef USE_API_SERVICES 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 global_api_server->register_user_service(service); +#else + static_assert( + sizeof(T) == 0, + "register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration"); +#endif } #else template void register_service(void (T::*callback)(), const std::string &name) { From 4d391fb27e850efb204152f80de29d72c3165e6d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 19:12:21 -0700 Subject: [PATCH 120/394] missing define for analyzer --- esphome/core/defines.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 8095ffed4a..97e766455a 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -123,6 +123,7 @@ #define USE_API_NOISE #define USE_API_PLAINTEXT #define USE_API_SERVICES +#define USE_API_CUSTOM_SERVICES #define API_MAX_SEND_QUEUE 8 #define USE_MD5 #define USE_SHA256 From 1577a46efdb6ab828c5b3efb7caa85888380d54d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 22:09:42 -0700 Subject: [PATCH 121/394] [gpio] Skip set_inverted() call for default false value (#11538) --- esphome/components/esp32/gpio.h | 6 +++--- esphome/components/esp32/gpio.py | 5 ++++- esphome/components/esp8266/gpio.h | 4 ++-- esphome/components/esp8266/gpio.py | 5 ++++- esphome/components/host/gpio.h | 4 ++-- esphome/components/host/gpio.py | 5 ++++- esphome/components/libretiny/gpio.py | 5 ++++- esphome/components/libretiny/gpio_arduino.h | 4 ++-- esphome/components/nrf52/gpio.py | 5 ++++- esphome/components/rp2040/gpio.h | 4 ++-- esphome/components/rp2040/gpio.py | 5 ++++- esphome/components/zephyr/gpio.h | 8 ++++---- 12 files changed, 39 insertions(+), 21 deletions(-) diff --git a/esphome/components/esp32/gpio.h b/esphome/components/esp32/gpio.h index 565e276ea8..d30f4bdcba 100644 --- a/esphome/components/esp32/gpio.h +++ b/esphome/components/esp32/gpio.h @@ -40,13 +40,13 @@ class ESP32InternalGPIOPin : public InternalGPIOPin { // - 3 bytes for members below // - 1 byte padding for alignment // - 4 bytes for vtable pointer - uint8_t pin_; // GPIO pin number (0-255, actual max ~54 on ESP32) - gpio::Flags flags_; // GPIO flags (1 byte) + uint8_t pin_; // GPIO pin number (0-255, actual max ~54 on ESP32) + gpio::Flags flags_{}; // GPIO flags (1 byte) struct PinFlags { uint8_t inverted : 1; // Invert pin logic (1 bit) uint8_t drive_strength : 2; // Drive strength 0-3 (2 bits) uint8_t reserved : 5; // Reserved for future use (5 bits) - } pin_flags_; // Total: 1 byte + } pin_flags_{}; // Total: 1 byte // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) static bool isr_service_installed; }; diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index 513f463d57..954891ea8d 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -223,7 +223,10 @@ async def esp32_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}"))) - cg.add(var.set_inverted(config[CONF_INVERTED])) + # Only set if true to avoid bloating setup() function + # (inverted bit in pin_flags_ bitfield is zero-initialized to false) + if config[CONF_INVERTED]: + cg.add(var.set_inverted(True)) if CONF_DRIVE_STRENGTH in config: cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH])) cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) diff --git a/esphome/components/esp8266/gpio.h b/esphome/components/esp8266/gpio.h index dd6407885e..a1b6d79b3b 100644 --- a/esphome/components/esp8266/gpio.h +++ b/esphome/components/esp8266/gpio.h @@ -29,8 +29,8 @@ class ESP8266GPIOPin : public InternalGPIOPin { void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; uint8_t pin_; - bool inverted_; - gpio::Flags flags_; + bool inverted_{}; + gpio::Flags flags_{}; }; } // namespace esp8266 diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index e7492fc505..2e8d6496bc 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -165,7 +165,10 @@ async def esp8266_pin_to_code(config): num = config[CONF_NUMBER] mode = config[CONF_MODE] cg.add(var.set_pin(num)) - cg.add(var.set_inverted(config[CONF_INVERTED])) + # Only set if true to avoid bloating setup() function + # (inverted bit in pin_flags_ bitfield is zero-initialized to false) + if config[CONF_INVERTED]: + cg.add(var.set_inverted(True)) cg.add(var.set_flags(pins.gpio_flags_expr(mode))) if num < 16: initial_state: PinInitialState = CORE.data[KEY_ESP8266][KEY_PIN_INITIAL_STATES][ diff --git a/esphome/components/host/gpio.h b/esphome/components/host/gpio.h index a60d535912..ae677291b9 100644 --- a/esphome/components/host/gpio.h +++ b/esphome/components/host/gpio.h @@ -28,8 +28,8 @@ class HostGPIOPin : public InternalGPIOPin { void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; uint8_t pin_; - bool inverted_; - gpio::Flags flags_; + bool inverted_{}; + gpio::Flags flags_{}; }; } // namespace host diff --git a/esphome/components/host/gpio.py b/esphome/components/host/gpio.py index 0f22a790bd..fcfb0b6c54 100644 --- a/esphome/components/host/gpio.py +++ b/esphome/components/host/gpio.py @@ -57,6 +57,9 @@ async def host_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(num)) - cg.add(var.set_inverted(config[CONF_INVERTED])) + # Only set if true to avoid bloating setup() function + # (inverted bit in pin_flags_ bitfield is zero-initialized to false) + if config[CONF_INVERTED]: + cg.add(var.set_inverted(True)) cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) return var diff --git a/esphome/components/libretiny/gpio.py b/esphome/components/libretiny/gpio.py index 07eb0ce133..9bad400eb7 100644 --- a/esphome/components/libretiny/gpio.py +++ b/esphome/components/libretiny/gpio.py @@ -199,6 +199,9 @@ async def component_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(num)) - cg.add(var.set_inverted(config[CONF_INVERTED])) + # Only set if true to avoid bloating setup() function + # (inverted bit in pin_flags_ bitfield is zero-initialized to false) + if config[CONF_INVERTED]: + cg.add(var.set_inverted(True)) cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) return var diff --git a/esphome/components/libretiny/gpio_arduino.h b/esphome/components/libretiny/gpio_arduino.h index 9adc425a41..3674748c18 100644 --- a/esphome/components/libretiny/gpio_arduino.h +++ b/esphome/components/libretiny/gpio_arduino.h @@ -27,8 +27,8 @@ class ArduinoInternalGPIOPin : public InternalGPIOPin { void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; uint8_t pin_; - bool inverted_; - gpio::Flags flags_; + bool inverted_{}; + gpio::Flags flags_{}; }; } // namespace libretiny diff --git a/esphome/components/nrf52/gpio.py b/esphome/components/nrf52/gpio.py index 260114f90e..17329042b2 100644 --- a/esphome/components/nrf52/gpio.py +++ b/esphome/components/nrf52/gpio.py @@ -74,6 +74,9 @@ async def nrf52_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(num)) - cg.add(var.set_inverted(config[CONF_INVERTED])) + # Only set if true to avoid bloating setup() function + # (inverted bit in pin_flags_ bitfield is zero-initialized to false) + if config[CONF_INVERTED]: + cg.add(var.set_inverted(True)) cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) return var diff --git a/esphome/components/rp2040/gpio.h b/esphome/components/rp2040/gpio.h index 9bc66d9e4b..47a6fe17f2 100644 --- a/esphome/components/rp2040/gpio.h +++ b/esphome/components/rp2040/gpio.h @@ -29,8 +29,8 @@ class RP2040GPIOPin : public InternalGPIOPin { void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; uint8_t pin_; - bool inverted_; - gpio::Flags flags_; + bool inverted_{}; + gpio::Flags flags_{}; }; } // namespace rp2040 diff --git a/esphome/components/rp2040/gpio.py b/esphome/components/rp2040/gpio.py index 58514f7db5..193e567d17 100644 --- a/esphome/components/rp2040/gpio.py +++ b/esphome/components/rp2040/gpio.py @@ -94,6 +94,9 @@ async def rp2040_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(num)) - cg.add(var.set_inverted(config[CONF_INVERTED])) + # Only set if true to avoid bloating setup() function + # (inverted bit in pin_flags_ bitfield is zero-initialized to false) + if config[CONF_INVERTED]: + cg.add(var.set_inverted(True)) cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) return var diff --git a/esphome/components/zephyr/gpio.h b/esphome/components/zephyr/gpio.h index f512ae4648..6e8f81857a 100644 --- a/esphome/components/zephyr/gpio.h +++ b/esphome/components/zephyr/gpio.h @@ -26,10 +26,10 @@ class ZephyrGPIOPin : public InternalGPIOPin { protected: void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; uint8_t pin_; - bool inverted_; - gpio::Flags flags_; - const device *gpio_ = nullptr; - bool value_ = false; + bool inverted_{}; + gpio::Flags flags_{}; + const device *gpio_{nullptr}; + bool value_{false}; }; } // namespace zephyr From 73d510d502a5603fb6aeff0ad96b786a6d3a81c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 00:35:09 -0700 Subject: [PATCH 122/394] Stateless lambdas --- esphome/automation.py | 12 ++++++ esphome/components/binary_sensor/__init__.py | 5 +++ esphome/components/binary_sensor/filter.h | 17 +++++++++ esphome/components/logger/__init__.py | 10 ++++- esphome/components/sensor/__init__.py | 5 +++ esphome/components/sensor/filter.h | 17 +++++++++ esphome/components/text_sensor/__init__.py | 5 +++ esphome/components/text_sensor/filter.h | 17 +++++++++ esphome/core/base_automation.h | 25 ++++++++++++ esphome/cpp_generator.py | 11 +++++- tests/unit_tests/test_cpp_generator.py | 40 ++++++++++++++++++++ 11 files changed, 162 insertions(+), 2 deletions(-) diff --git a/esphome/automation.py b/esphome/automation.py index 99def9f273..a75fcf35a4 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -87,6 +87,7 @@ def validate_potentially_or_condition(value): DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component) LambdaAction = cg.esphome_ns.class_("LambdaAction", Action) +StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action) IfAction = cg.esphome_ns.class_("IfAction", Action) WhileAction = cg.esphome_ns.class_("WhileAction", Action) RepeatAction = cg.esphome_ns.class_("RepeatAction", Action) @@ -97,6 +98,7 @@ ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action) Automation = cg.esphome_ns.class_("Automation") LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition) +StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition) ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component) @@ -240,6 +242,11 @@ async def lambda_condition_to_code( args: TemplateArgsType, ) -> MockObj: lambda_ = await cg.process_lambda(config, args, return_type=bool) + # Use optimized StatelessLambdaCondition for lambdas with no capture + if lambda_.capture == "": + # Override the condition_id type to use StatelessLambdaCondition + condition_id = condition_id.copy() + condition_id.type = StatelessLambdaCondition return cg.new_Pvariable(condition_id, template_arg, lambda_) @@ -406,6 +413,11 @@ async def lambda_action_to_code( args: TemplateArgsType, ) -> MockObj: lambda_ = await cg.process_lambda(config, args, return_type=cg.void) + # Use optimized StatelessLambdaAction for lambdas with no capture + if lambda_.capture == "": + # Override the action_id type to use StatelessLambdaAction + action_id = action_id.copy() + action_id.type = StatelessLambdaAction return cg.new_Pvariable(action_id, template_arg, lambda_) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 26e784a0b8..9e87adf1d1 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -155,6 +155,7 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter) AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component) LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter) +StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter) SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component) _LOGGER = getLogger(__name__) @@ -299,6 +300,10 @@ async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(bool, "x")], return_type=cg.optional.template(bool) ) + # Use optimized StatelessLambdaFilter for lambdas with no capture + if lambda_.capture == "": + filter_id = filter_id.copy() + filter_id.type = StatelessLambdaFilter return cg.new_Pvariable(filter_id, lambda_) diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index a7eb080feb..7ee253ead5 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -111,6 +111,23 @@ class LambdaFilter : public Filter { std::function(bool)> f_; }; +/** Optimized lambda filter for stateless lambdas (no capture). + * + * Uses function pointer instead of std::function to reduce memory overhead. + * Memory: 8 bytes (function pointer) vs 32 bytes (std::function). + */ +class StatelessLambdaFilter : public Filter { + public: + using stateless_lambda_filter_t = optional (*)(bool); + + explicit StatelessLambdaFilter(stateless_lambda_filter_t f) : f_(f) {} + + optional new_value(bool value) override { return this->f_(value); } + + protected: + stateless_lambda_filter_t f_; +}; + class SettleFilter : public Filter, public Component { public: optional new_value(bool value) override; diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 1d02073d27..61c9ef5051 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -1,7 +1,7 @@ import re from esphome import automation -from esphome.automation import LambdaAction +from esphome.automation import LambdaAction, StatelessLambdaAction import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant from esphome.components.esp32.const import ( @@ -430,6 +430,10 @@ async def logger_log_action_to_code(config, action_id, template_arg, args): text = str(cg.statement(esp_log(config[CONF_TAG], config[CONF_FORMAT], *args_))) lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) + # Use optimized StatelessLambdaAction for lambdas with no capture + if lambda_.capture == "": + action_id = action_id.copy() + action_id.type = StatelessLambdaAction return cg.new_Pvariable(action_id, template_arg, lambda_) @@ -455,6 +459,10 @@ async def logger_set_level_to_code(config, action_id, template_arg, args): text = str(cg.statement(logger.set_log_level(level))) lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) + # Use optimized StatelessLambdaAction for lambdas with no capture + if lambda_.capture == "": + action_id = action_id.copy() + action_id.type = StatelessLambdaAction return cg.new_Pvariable(action_id, template_arg, lambda_) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 93283e4d47..41e9eb63d5 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -261,6 +261,7 @@ ExponentialMovingAverageFilter = sensor_ns.class_( ) ThrottleAverageFilter = sensor_ns.class_("ThrottleAverageFilter", Filter, cg.Component) LambdaFilter = sensor_ns.class_("LambdaFilter", Filter) +StatelessLambdaFilter = sensor_ns.class_("StatelessLambdaFilter", Filter) OffsetFilter = sensor_ns.class_("OffsetFilter", Filter) MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter) ValueListFilter = sensor_ns.class_("ValueListFilter", Filter) @@ -573,6 +574,10 @@ async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(float, "x")], return_type=cg.optional.template(float) ) + # Use optimized StatelessLambdaFilter for lambdas with no capture + if lambda_.capture == "": + filter_id = filter_id.copy() + filter_id.type = StatelessLambdaFilter return cg.new_Pvariable(filter_id, lambda_) diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index ecd55308d1..4f0840a75e 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -296,6 +296,23 @@ class LambdaFilter : public Filter { lambda_filter_t lambda_filter_; }; +/** Optimized lambda filter for stateless lambdas (no capture). + * + * Uses function pointer instead of std::function to reduce memory overhead. + * Memory: 8 bytes (function pointer) vs 32 bytes (std::function). + */ +class StatelessLambdaFilter : public Filter { + public: + using stateless_lambda_filter_t = optional (*)(float); + + explicit StatelessLambdaFilter(stateless_lambda_filter_t lambda_filter) : lambda_filter_(lambda_filter) {} + + optional new_value(float value) override { return this->lambda_filter_(value); } + + protected: + stateless_lambda_filter_t lambda_filter_; +}; + /// A simple filter that adds `offset` to each value it receives. class OffsetFilter : public Filter { public: diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 7a9e947abd..afd9fc1733 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -57,6 +57,7 @@ validate_filters = cv.validate_registry("filter", FILTER_REGISTRY) # Filters Filter = text_sensor_ns.class_("Filter") LambdaFilter = text_sensor_ns.class_("LambdaFilter", Filter) +StatelessLambdaFilter = text_sensor_ns.class_("StatelessLambdaFilter", Filter) ToUpperFilter = text_sensor_ns.class_("ToUpperFilter", Filter) ToLowerFilter = text_sensor_ns.class_("ToLowerFilter", Filter) AppendFilter = text_sensor_ns.class_("AppendFilter", Filter) @@ -70,6 +71,10 @@ async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(cg.std_string, "x")], return_type=cg.optional.template(cg.std_string) ) + # Use optimized StatelessLambdaFilter for lambdas with no capture + if lambda_.capture == "": + filter_id = filter_id.copy() + filter_id.type = StatelessLambdaFilter return cg.new_Pvariable(filter_id, lambda_) diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index c77c221235..d8c71379ad 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -62,6 +62,23 @@ class LambdaFilter : public Filter { lambda_filter_t lambda_filter_; }; +/** Optimized lambda filter for stateless lambdas (no capture). + * + * Uses function pointer instead of std::function to reduce memory overhead. + * Memory: 8 bytes (function pointer) vs 32 bytes (std::function). + */ +class StatelessLambdaFilter : public Filter { + public: + using stateless_lambda_filter_t = optional (*)(std::string); + + explicit StatelessLambdaFilter(stateless_lambda_filter_t lambda_filter) : lambda_filter_(lambda_filter) {} + + optional new_value(std::string value) override { return this->lambda_filter_(value); } + + protected: + stateless_lambda_filter_t lambda_filter_; +}; + /// A simple filter that converts all text to uppercase class ToUpperFilter : public Filter { public: diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index af8cde971b..683be2a9e9 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -79,6 +79,18 @@ template class LambdaCondition : public Condition { std::function f_; }; +/// Optimized lambda condition for stateless lambdas (no capture). +/// Uses function pointer instead of std::function to reduce memory overhead. +/// Memory: 8 bytes (function pointer) vs 32 bytes (std::function). +template class StatelessLambdaCondition : public Condition { + public: + explicit StatelessLambdaCondition(bool (*f)(Ts...)) : f_(f) {} + bool check(Ts... x) override { return this->f_(x...); } + + protected: + bool (*f_)(Ts...); +}; + template class ForCondition : public Condition, public Component { public: explicit ForCondition(Condition<> *condition) : condition_(condition) {} @@ -190,6 +202,19 @@ template class LambdaAction : public Action { std::function f_; }; +/// Optimized lambda action for stateless lambdas (no capture). +/// Uses function pointer instead of std::function to reduce memory overhead. +/// Memory: 8 bytes (function pointer) vs 32 bytes (std::function). +template class StatelessLambdaAction : public Action { + public: + explicit StatelessLambdaAction(void (*f)(Ts...)) : f_(f) {} + + void play(Ts... x) override { this->f_(x...); } + + protected: + void (*f_)(Ts...); +}; + template class IfAction : public Action { public: explicit IfAction(Condition *condition) : condition_(condition) {} diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index b2022c7ae6..4e286dfa2c 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -198,7 +198,10 @@ class LambdaExpression(Expression): self.return_type = safe_exp(return_type) if return_type is not None else None def __str__(self): - cpp = f"[{self.capture}]({self.parameters})" + # Unary + converts stateless lambda to function pointer + # This allows implicit conversion to void (*)() or bool (*)() + prefix = "+" if self.capture == "" else "" + cpp = f"{prefix}[{self.capture}]({self.parameters})" if self.return_type is not None: cpp += f" -> {self.return_type}" cpp += " {\n" @@ -700,6 +703,12 @@ async def process_lambda( parts[i * 3 + 1] = var parts[i * 3 + 2] = "" + # All id() references are global variables in generated C++ code. + # Global variables should not be captured - they're accessible everywhere. + # Use empty capture instead of capture-by-value. + if capture == "=": + capture = "" + if isinstance(value, ESPHomeDataBase) and value.esp_range is not None: location = value.esp_range.start_mark location.line += value.content_offset diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 95633ca0c6..b495b52064 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -173,6 +173,46 @@ class TestLambdaExpression: "}" ) + def test_str__stateless_no_return(self): + """Test stateless lambda (empty capture) gets unary + prefix""" + target = cg.LambdaExpression( + ('ESP_LOGD("main", "Test message");',), + (), # No parameters + "", # Empty capture (stateless) + ) + + actual = str(target) + + assert actual == ('+[]() {\n ESP_LOGD("main", "Test message");\n}') + + def test_str__stateless_with_return(self): + """Test stateless lambda with return type gets unary + prefix""" + target = cg.LambdaExpression( + ("return global_value > 0;",), + (), # No parameters + "", # Empty capture (stateless) + bool, # Return type + ) + + actual = str(target) + + assert actual == ("+[]() -> bool {\n return global_value > 0;\n}") + + def test_str__stateless_with_params(self): + """Test stateless lambda with parameters gets unary + prefix""" + target = cg.LambdaExpression( + ("return foo + bar;",), + ((int, "foo"), (float, "bar")), + "", # Empty capture (stateless) + float, + ) + + actual = str(target) + + assert actual == ( + "+[](int32_t foo, float bar) -> float {\n return foo + bar;\n}" + ) + class TestLiterals: @pytest.mark.parametrize( From 7737689774908ba6c3b32eb2f5bbcc206afb9c80 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 00:56:22 -0700 Subject: [PATCH 123/394] dry --- esphome/automation.py | 33 ++++++++++++++------ esphome/components/binary_sensor/__init__.py | 7 ++--- esphome/components/logger/__init__.py | 14 ++++----- esphome/components/sensor/__init__.py | 7 ++--- esphome/components/text_sensor/__init__.py | 7 ++--- 5 files changed, 38 insertions(+), 30 deletions(-) diff --git a/esphome/automation.py b/esphome/automation.py index a75fcf35a4..990c50b2f7 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -102,6 +102,23 @@ StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Cond ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component) +def use_stateless_lambda_if_applicable(id_obj, lambda_expr, stateless_class): + """Replace ID type with stateless lambda class if lambda has no capture. + + Args: + id_obj: The ID object (action_id, condition_id, or filter_id) + lambda_expr: The lambda expression object + stateless_class: The stateless class to use (StatelessLambdaAction, StatelessLambdaCondition, or StatelessLambdaFilter) + + Returns: + The original ID or a copy with type replaced to use the stateless class + """ + if lambda_expr.capture == "": + id_obj = id_obj.copy() + id_obj.type = stateless_class + return id_obj + + def validate_automation(extra_schema=None, extra_validators=None, single=False): if extra_schema is None: extra_schema = {} @@ -242,11 +259,9 @@ async def lambda_condition_to_code( args: TemplateArgsType, ) -> MockObj: lambda_ = await cg.process_lambda(config, args, return_type=bool) - # Use optimized StatelessLambdaCondition for lambdas with no capture - if lambda_.capture == "": - # Override the condition_id type to use StatelessLambdaCondition - condition_id = condition_id.copy() - condition_id.type = StatelessLambdaCondition + condition_id = use_stateless_lambda_if_applicable( + condition_id, lambda_, StatelessLambdaCondition + ) return cg.new_Pvariable(condition_id, template_arg, lambda_) @@ -413,11 +428,9 @@ async def lambda_action_to_code( args: TemplateArgsType, ) -> MockObj: lambda_ = await cg.process_lambda(config, args, return_type=cg.void) - # Use optimized StatelessLambdaAction for lambdas with no capture - if lambda_.capture == "": - # Override the action_id type to use StatelessLambdaAction - action_id = action_id.copy() - action_id.type = StatelessLambdaAction + action_id = use_stateless_lambda_if_applicable( + action_id, lambda_, StatelessLambdaAction + ) return cg.new_Pvariable(action_id, template_arg, lambda_) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 9e87adf1d1..5fb1ee6667 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -300,10 +300,9 @@ async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(bool, "x")], return_type=cg.optional.template(bool) ) - # Use optimized StatelessLambdaFilter for lambdas with no capture - if lambda_.capture == "": - filter_id = filter_id.copy() - filter_id.type = StatelessLambdaFilter + filter_id = automation.use_stateless_lambda_if_applicable( + filter_id, lambda_, StatelessLambdaFilter + ) return cg.new_Pvariable(filter_id, lambda_) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 61c9ef5051..2ae6a7fc38 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -430,10 +430,9 @@ async def logger_log_action_to_code(config, action_id, template_arg, args): text = str(cg.statement(esp_log(config[CONF_TAG], config[CONF_FORMAT], *args_))) lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) - # Use optimized StatelessLambdaAction for lambdas with no capture - if lambda_.capture == "": - action_id = action_id.copy() - action_id.type = StatelessLambdaAction + action_id = automation.use_stateless_lambda_if_applicable( + action_id, lambda_, StatelessLambdaAction + ) return cg.new_Pvariable(action_id, template_arg, lambda_) @@ -459,10 +458,9 @@ async def logger_set_level_to_code(config, action_id, template_arg, args): text = str(cg.statement(logger.set_log_level(level))) lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) - # Use optimized StatelessLambdaAction for lambdas with no capture - if lambda_.capture == "": - action_id = action_id.copy() - action_id.type = StatelessLambdaAction + action_id = automation.use_stateless_lambda_if_applicable( + action_id, lambda_, StatelessLambdaAction + ) return cg.new_Pvariable(action_id, template_arg, lambda_) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 41e9eb63d5..ffa33d521c 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -574,10 +574,9 @@ async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(float, "x")], return_type=cg.optional.template(float) ) - # Use optimized StatelessLambdaFilter for lambdas with no capture - if lambda_.capture == "": - filter_id = filter_id.copy() - filter_id.type = StatelessLambdaFilter + filter_id = automation.use_stateless_lambda_if_applicable( + filter_id, lambda_, StatelessLambdaFilter + ) return cg.new_Pvariable(filter_id, lambda_) diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index afd9fc1733..47259667f4 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -71,10 +71,9 @@ async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(cg.std_string, "x")], return_type=cg.optional.template(cg.std_string) ) - # Use optimized StatelessLambdaFilter for lambdas with no capture - if lambda_.capture == "": - filter_id = filter_id.copy() - filter_id.type = StatelessLambdaFilter + filter_id = automation.use_stateless_lambda_if_applicable( + filter_id, lambda_, StatelessLambdaFilter + ) return cg.new_Pvariable(filter_id, lambda_) From 9e77ece7ceaf45f2377217ff2352574fea1acae8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 00:58:52 -0700 Subject: [PATCH 124/394] dry --- esphome/automation.py | 24 ++++++++++++++++-------- esphome/components/logger/__init__.py | 18 ++++++++++++------ 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/esphome/automation.py b/esphome/automation.py index 990c50b2f7..462489f590 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -103,7 +103,11 @@ ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component) def use_stateless_lambda_if_applicable(id_obj, lambda_expr, stateless_class): - """Replace ID type with stateless lambda class if lambda has no capture. + """Return appropriate ID for lambda based on whether it has capture. + + For stateless lambdas (empty capture), returns a copy of id_obj with type + set to stateless_class to use function pointer instead of std::function. + Otherwise returns the original ID unchanged. Args: id_obj: The ID object (action_id, condition_id, or filter_id) @@ -111,7 +115,7 @@ def use_stateless_lambda_if_applicable(id_obj, lambda_expr, stateless_class): stateless_class: The stateless class to use (StatelessLambdaAction, StatelessLambdaCondition, or StatelessLambdaFilter) Returns: - The original ID or a copy with type replaced to use the stateless class + ID to use with cg.new_Pvariable() - either original or modified copy """ if lambda_expr.capture == "": id_obj = id_obj.copy() @@ -259,10 +263,13 @@ async def lambda_condition_to_code( args: TemplateArgsType, ) -> MockObj: lambda_ = await cg.process_lambda(config, args, return_type=bool) - condition_id = use_stateless_lambda_if_applicable( - condition_id, lambda_, StatelessLambdaCondition + return cg.new_Pvariable( + use_stateless_lambda_if_applicable( + condition_id, lambda_, StatelessLambdaCondition + ), + template_arg, + lambda_, ) - return cg.new_Pvariable(condition_id, template_arg, lambda_) @register_condition( @@ -428,10 +435,11 @@ async def lambda_action_to_code( args: TemplateArgsType, ) -> MockObj: lambda_ = await cg.process_lambda(config, args, return_type=cg.void) - action_id = use_stateless_lambda_if_applicable( - action_id, lambda_, StatelessLambdaAction + return cg.new_Pvariable( + use_stateless_lambda_if_applicable(action_id, lambda_, StatelessLambdaAction), + template_arg, + lambda_, ) - return cg.new_Pvariable(action_id, template_arg, lambda_) @register_action( diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 2ae6a7fc38..e7715cfc10 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -430,10 +430,13 @@ async def logger_log_action_to_code(config, action_id, template_arg, args): text = str(cg.statement(esp_log(config[CONF_TAG], config[CONF_FORMAT], *args_))) lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) - action_id = automation.use_stateless_lambda_if_applicable( - action_id, lambda_, StatelessLambdaAction + return cg.new_Pvariable( + automation.use_stateless_lambda_if_applicable( + action_id, lambda_, StatelessLambdaAction + ), + template_arg, + lambda_, ) - return cg.new_Pvariable(action_id, template_arg, lambda_) @automation.register_action( @@ -458,10 +461,13 @@ async def logger_set_level_to_code(config, action_id, template_arg, args): text = str(cg.statement(logger.set_log_level(level))) lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) - action_id = automation.use_stateless_lambda_if_applicable( - action_id, lambda_, StatelessLambdaAction + return cg.new_Pvariable( + automation.use_stateless_lambda_if_applicable( + action_id, lambda_, StatelessLambdaAction + ), + template_arg, + lambda_, ) - return cg.new_Pvariable(action_id, template_arg, lambda_) FILTER_SOURCE_FILES = filter_source_files_from_platform( From 23207f00743e86a8d0ea4f31c418229e96a1ecf6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 01:03:15 -0700 Subject: [PATCH 125/394] dry --- esphome/automation.py | 46 +++++++++++--------- esphome/components/binary_sensor/__init__.py | 5 +-- esphome/components/logger/__init__.py | 16 ++----- esphome/components/sensor/__init__.py | 5 +-- esphome/components/text_sensor/__init__.py | 5 +-- tests/component_tests/text/test_text.py | 4 +- 6 files changed, 35 insertions(+), 46 deletions(-) diff --git a/esphome/automation.py b/esphome/automation.py index 462489f590..cfe0af1b59 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -16,7 +16,12 @@ from esphome.const import ( CONF_UPDATE_INTERVAL, ) from esphome.core import ID -from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType +from esphome.cpp_generator import ( + LambdaExpression, + MockObj, + MockObjClass, + TemplateArgsType, +) from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.types import ConfigType from esphome.util import Registry @@ -102,25 +107,34 @@ StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Cond ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component) -def use_stateless_lambda_if_applicable(id_obj, lambda_expr, stateless_class): - """Return appropriate ID for lambda based on whether it has capture. +def new_lambda_pvariable( + id_obj: ID, + lambda_expr: LambdaExpression, + stateless_class: MockObjClass, + template_arg: cg.TemplateArguments | None = None, +) -> MockObj: + """Create Pvariable for lambda, using stateless class if applicable. - For stateless lambdas (empty capture), returns a copy of id_obj with type - set to stateless_class to use function pointer instead of std::function. - Otherwise returns the original ID unchanged. + Combines ID selection and Pvariable creation in one call. For stateless + lambdas (empty capture), uses function pointer instead of std::function. Args: id_obj: The ID object (action_id, condition_id, or filter_id) lambda_expr: The lambda expression object - stateless_class: The stateless class to use (StatelessLambdaAction, StatelessLambdaCondition, or StatelessLambdaFilter) + stateless_class: The stateless class to use for stateless lambdas + template_arg: Optional template arguments (for actions/conditions) Returns: - ID to use with cg.new_Pvariable() - either original or modified copy + The created Pvariable """ + # For stateless lambdas, use function pointer instead of std::function if lambda_expr.capture == "": id_obj = id_obj.copy() id_obj.type = stateless_class - return id_obj + + if template_arg is not None: + return cg.new_Pvariable(id_obj, template_arg, lambda_expr) + return cg.new_Pvariable(id_obj, lambda_expr) def validate_automation(extra_schema=None, extra_validators=None, single=False): @@ -263,12 +277,8 @@ async def lambda_condition_to_code( args: TemplateArgsType, ) -> MockObj: lambda_ = await cg.process_lambda(config, args, return_type=bool) - return cg.new_Pvariable( - use_stateless_lambda_if_applicable( - condition_id, lambda_, StatelessLambdaCondition - ), - template_arg, - lambda_, + return new_lambda_pvariable( + condition_id, lambda_, StatelessLambdaCondition, template_arg ) @@ -435,11 +445,7 @@ async def lambda_action_to_code( args: TemplateArgsType, ) -> MockObj: lambda_ = await cg.process_lambda(config, args, return_type=cg.void) - return cg.new_Pvariable( - use_stateless_lambda_if_applicable(action_id, lambda_, StatelessLambdaAction), - template_arg, - lambda_, - ) + return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg) @register_action( diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 5fb1ee6667..8892b57e6e 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -300,10 +300,7 @@ async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(bool, "x")], return_type=cg.optional.template(bool) ) - filter_id = automation.use_stateless_lambda_if_applicable( - filter_id, lambda_, StatelessLambdaFilter - ) - return cg.new_Pvariable(filter_id, lambda_) + return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) @register_filter( diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index e7715cfc10..22bf3d2f4c 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -430,12 +430,8 @@ async def logger_log_action_to_code(config, action_id, template_arg, args): text = str(cg.statement(esp_log(config[CONF_TAG], config[CONF_FORMAT], *args_))) lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) - return cg.new_Pvariable( - automation.use_stateless_lambda_if_applicable( - action_id, lambda_, StatelessLambdaAction - ), - template_arg, - lambda_, + return automation.new_lambda_pvariable( + action_id, lambda_, StatelessLambdaAction, template_arg ) @@ -461,12 +457,8 @@ async def logger_set_level_to_code(config, action_id, template_arg, args): text = str(cg.statement(logger.set_log_level(level))) lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) - return cg.new_Pvariable( - automation.use_stateless_lambda_if_applicable( - action_id, lambda_, StatelessLambdaAction - ), - template_arg, - lambda_, + return automation.new_lambda_pvariable( + action_id, lambda_, StatelessLambdaAction, template_arg ) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index ffa33d521c..41ac3516b9 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -574,10 +574,7 @@ async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(float, "x")], return_type=cg.optional.template(float) ) - filter_id = automation.use_stateless_lambda_if_applicable( - filter_id, lambda_, StatelessLambdaFilter - ) - return cg.new_Pvariable(filter_id, lambda_) + return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) DELTA_SCHEMA = cv.Schema( diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 47259667f4..adc8a76fcd 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -71,10 +71,7 @@ async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(cg.std_string, "x")], return_type=cg.optional.template(cg.std_string) ) - filter_id = automation.use_stateless_lambda_if_applicable( - filter_id, lambda_, StatelessLambdaFilter - ) - return cg.new_Pvariable(filter_id, lambda_) + return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) @FILTER_REGISTRY.register("to_upper", ToUpperFilter, {}) diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 75f1c4b88b..6eec86de9f 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -58,7 +58,7 @@ def test_text_config_value_mode_set(generate_main): def test_text_config_lamda_is_set(generate_main): """ - Test if lambda is set for lambda mode + Test if lambda is set for lambda mode (optimized with stateless lambda) """ # Given @@ -66,5 +66,5 @@ def test_text_config_lamda_is_set(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert "it_4->set_template([=]() -> esphome::optional {" in main_cpp + assert "it_4->set_template(+[]() -> esphome::optional {" in main_cpp assert 'return std::string{"Hello"};' in main_cpp From c30e130a485ebd672d2c6c9499767a6f3272581d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 01:07:08 -0700 Subject: [PATCH 126/394] dry --- esphome/components/binary_sensor/filter.h | 6 ++---- esphome/components/sensor/filter.h | 6 ++---- esphome/components/text_sensor/filter.h | 6 ++---- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 7ee253ead5..c1c54709a9 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -118,14 +118,12 @@ class LambdaFilter : public Filter { */ class StatelessLambdaFilter : public Filter { public: - using stateless_lambda_filter_t = optional (*)(bool); - - explicit StatelessLambdaFilter(stateless_lambda_filter_t f) : f_(f) {} + explicit StatelessLambdaFilter(optional (*f)(bool)) : f_(f) {} optional new_value(bool value) override { return this->f_(value); } protected: - stateless_lambda_filter_t f_; + optional (*f_)(bool); }; class SettleFilter : public Filter, public Component { diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 4f0840a75e..ebcdbb8cab 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -303,14 +303,12 @@ class LambdaFilter : public Filter { */ class StatelessLambdaFilter : public Filter { public: - using stateless_lambda_filter_t = optional (*)(float); - - explicit StatelessLambdaFilter(stateless_lambda_filter_t lambda_filter) : lambda_filter_(lambda_filter) {} + explicit StatelessLambdaFilter(optional (*lambda_filter)(float)) : lambda_filter_(lambda_filter) {} optional new_value(float value) override { return this->lambda_filter_(value); } protected: - stateless_lambda_filter_t lambda_filter_; + optional (*lambda_filter_)(float); }; /// A simple filter that adds `offset` to each value it receives. diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index d8c71379ad..dddf1b2b34 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -69,14 +69,12 @@ class LambdaFilter : public Filter { */ class StatelessLambdaFilter : public Filter { public: - using stateless_lambda_filter_t = optional (*)(std::string); - - explicit StatelessLambdaFilter(stateless_lambda_filter_t lambda_filter) : lambda_filter_(lambda_filter) {} + explicit StatelessLambdaFilter(optional (*lambda_filter)(std::string)) : lambda_filter_(lambda_filter) {} optional new_value(std::string value) override { return this->lambda_filter_(value); } protected: - stateless_lambda_filter_t lambda_filter_; + optional (*lambda_filter_)(std::string); }; /// A simple filter that converts all text to uppercase From 97346e5644b7df53a3b4aa83f4316db2bd680799 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 01:30:39 -0700 Subject: [PATCH 127/394] tweak --- esphome/components/binary_sensor/filter.h | 2 +- esphome/components/sensor/filter.h | 2 +- esphome/components/text_sensor/filter.h | 2 +- esphome/core/base_automation.h | 4 ++-- tests/unit_tests/test_cpp_generator.py | 17 +++++++++++++++++ 5 files changed, 22 insertions(+), 5 deletions(-) diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index c1c54709a9..2d473c3b64 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -114,7 +114,7 @@ class LambdaFilter : public Filter { /** Optimized lambda filter for stateless lambdas (no capture). * * Uses function pointer instead of std::function to reduce memory overhead. - * Memory: 8 bytes (function pointer) vs 32 bytes (std::function). + * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). */ class StatelessLambdaFilter : public Filter { public: diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index ebcdbb8cab..75e28a1efe 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -299,7 +299,7 @@ class LambdaFilter : public Filter { /** Optimized lambda filter for stateless lambdas (no capture). * * Uses function pointer instead of std::function to reduce memory overhead. - * Memory: 8 bytes (function pointer) vs 32 bytes (std::function). + * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). */ class StatelessLambdaFilter : public Filter { public: diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index dddf1b2b34..85acac5c8d 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -65,7 +65,7 @@ class LambdaFilter : public Filter { /** Optimized lambda filter for stateless lambdas (no capture). * * Uses function pointer instead of std::function to reduce memory overhead. - * Memory: 8 bytes (function pointer) vs 32 bytes (std::function). + * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). */ class StatelessLambdaFilter : public Filter { public: diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 683be2a9e9..1c60dd1c7a 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -81,7 +81,7 @@ template class LambdaCondition : public Condition { /// Optimized lambda condition for stateless lambdas (no capture). /// Uses function pointer instead of std::function to reduce memory overhead. -/// Memory: 8 bytes (function pointer) vs 32 bytes (std::function). +/// Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). template class StatelessLambdaCondition : public Condition { public: explicit StatelessLambdaCondition(bool (*f)(Ts...)) : f_(f) {} @@ -204,7 +204,7 @@ template class LambdaAction : public Action { /// Optimized lambda action for stateless lambdas (no capture). /// Uses function pointer instead of std::function to reduce memory overhead. -/// Memory: 8 bytes (function pointer) vs 32 bytes (std::function). +/// Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). template class StatelessLambdaAction : public Action { public: explicit StatelessLambdaAction(void (*f)(Ts...)) : f_(f) {} diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index b495b52064..f42ad180c7 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -213,6 +213,23 @@ class TestLambdaExpression: "+[](int32_t foo, float bar) -> float {\n return foo + bar;\n}" ) + def test_str__with_capture_no_prefix(self): + """Test lambda with capture (not stateless) does NOT get + prefix""" + target = cg.LambdaExpression( + ("return captured_var + x;",), + ((int, "x"),), + "captured_var", # Has capture (not stateless) + int, + ) + + actual = str(target) + + # Should NOT have + prefix + assert actual == ( + "[captured_var](int32_t x) -> int32_t {\n return captured_var + x;\n}" + ) + assert not actual.startswith("+") + class TestLiterals: @pytest.mark.parametrize( From 7394cbf77329f3b5a035e5ce334c9ac0ae19818c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 26 Oct 2025 09:00:08 -0400 Subject: [PATCH 128/394] [core] Don't allow python 3.14 (#11527) --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b7b4a48d7e..49598d434d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,9 @@ classifiers = [ "Programming Language :: Python :: 3", "Topic :: Home Automation", ] -requires-python = ">=3.11.0" + +# Python 3.14 is currently not supported by IDF <= 5.5.1, see https://github.com/esphome/esphome/issues/11502 +requires-python = ">=3.11.0,<3.14" dynamic = ["dependencies", "optional-dependencies", "version"] From 5e4a551a77627d3b66f4d331b2de19b7ec8b719f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 09:32:43 -0700 Subject: [PATCH 129/394] over engineered --- esphome/cpp_generator.py | 7 +++---- tests/component_tests/text/test_text.py | 2 +- tests/unit_tests/test_cpp_generator.py | 18 ++++++++---------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 4e286dfa2c..a2da424e5a 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -198,10 +198,9 @@ class LambdaExpression(Expression): self.return_type = safe_exp(return_type) if return_type is not None else None def __str__(self): - # Unary + converts stateless lambda to function pointer - # This allows implicit conversion to void (*)() or bool (*)() - prefix = "+" if self.capture == "" else "" - cpp = f"{prefix}[{self.capture}]({self.parameters})" + # Stateless lambdas (empty capture) implicitly convert to function pointers + # when assigned to function pointer types - no unary + needed + cpp = f"[{self.capture}]({self.parameters})" if self.return_type is not None: cpp += f" -> {self.return_type}" cpp += " {\n" diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 6eec86de9f..99ddd78ee7 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -66,5 +66,5 @@ def test_text_config_lamda_is_set(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert "it_4->set_template(+[]() -> esphome::optional {" in main_cpp + assert "it_4->set_template([]() -> esphome::optional {" in main_cpp assert 'return std::string{"Hello"};' in main_cpp diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index f42ad180c7..2c9f760c8e 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -174,7 +174,7 @@ class TestLambdaExpression: ) def test_str__stateless_no_return(self): - """Test stateless lambda (empty capture) gets unary + prefix""" + """Test stateless lambda (empty capture) generates correctly""" target = cg.LambdaExpression( ('ESP_LOGD("main", "Test message");',), (), # No parameters @@ -183,10 +183,10 @@ class TestLambdaExpression: actual = str(target) - assert actual == ('+[]() {\n ESP_LOGD("main", "Test message");\n}') + assert actual == ('[]() {\n ESP_LOGD("main", "Test message");\n}') def test_str__stateless_with_return(self): - """Test stateless lambda with return type gets unary + prefix""" + """Test stateless lambda with return type generates correctly""" target = cg.LambdaExpression( ("return global_value > 0;",), (), # No parameters @@ -196,10 +196,10 @@ class TestLambdaExpression: actual = str(target) - assert actual == ("+[]() -> bool {\n return global_value > 0;\n}") + assert actual == ("[]() -> bool {\n return global_value > 0;\n}") def test_str__stateless_with_params(self): - """Test stateless lambda with parameters gets unary + prefix""" + """Test stateless lambda with parameters generates correctly""" target = cg.LambdaExpression( ("return foo + bar;",), ((int, "foo"), (float, "bar")), @@ -210,11 +210,11 @@ class TestLambdaExpression: actual = str(target) assert actual == ( - "+[](int32_t foo, float bar) -> float {\n return foo + bar;\n}" + "[](int32_t foo, float bar) -> float {\n return foo + bar;\n}" ) - def test_str__with_capture_no_prefix(self): - """Test lambda with capture (not stateless) does NOT get + prefix""" + def test_str__with_capture(self): + """Test lambda with capture generates correctly""" target = cg.LambdaExpression( ("return captured_var + x;",), ((int, "x"),), @@ -224,11 +224,9 @@ class TestLambdaExpression: actual = str(target) - # Should NOT have + prefix assert actual == ( "[captured_var](int32_t x) -> int32_t {\n return captured_var + x;\n}" ) - assert not actual.startswith("+") class TestLiterals: From ddf86b4e77820e1077225dfb6cec00b631e64309 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 11:31:55 -0700 Subject: [PATCH 130/394] wip --- esphome/core/automation.h | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 0512752d50..fe2daa4f80 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -31,7 +31,16 @@ template class TemplatableValue { new (&this->value_) T(std::move(value)); } - template::value, int> = 0> TemplatableValue(F f) : type_(LAMBDA) { + // For stateless lambdas (convertible to function pointer): use function pointer + template::value && std::is_convertible::value, int> = 0> + TemplatableValue(F f) : type_(STATELESS_LAMBDA) { + this->stateless_f_ = f; // Implicit conversion to function pointer + } + + // For stateful lambdas (not convertible to function pointer): use std::function + template::value && !std::is_convertible::value, int> = 0> + TemplatableValue(F f) : type_(LAMBDA) { this->f_ = new std::function(std::move(f)); } @@ -41,6 +50,8 @@ template class TemplatableValue { new (&this->value_) T(other.value_); } else if (type_ == LAMBDA) { this->f_ = new std::function(*other.f_); + } else if (type_ == STATELESS_LAMBDA) { + this->stateless_f_ = other.stateless_f_; } } @@ -51,6 +62,8 @@ template class TemplatableValue { } else if (type_ == LAMBDA) { this->f_ = other.f_; other.f_ = nullptr; + } else if (type_ == STATELESS_LAMBDA) { + this->stateless_f_ = other.stateless_f_; } other.type_ = NONE; } @@ -78,13 +91,17 @@ template class TemplatableValue { } else if (type_ == LAMBDA) { delete this->f_; } + // STATELESS_LAMBDA needs no cleanup (function pointer, not heap-allocated) } bool has_value() { return this->type_ != NONE; } T value(X... x) { + if (this->type_ == STATELESS_LAMBDA) { + return this->stateless_f_(x...); // Direct function pointer call + } if (this->type_ == LAMBDA) { - return (*this->f_)(x...); + return (*this->f_)(x...); // std::function call } // return value also when none return this->type_ == VALUE ? this->value_ : T{}; @@ -109,11 +126,13 @@ template class TemplatableValue { NONE, VALUE, LAMBDA, + STATELESS_LAMBDA, } type_; union { T value_; std::function *f_; + T (*stateless_f_)(X...); }; }; From 077bd624f08712355b6998b8269ed153272e88e5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 11:32:59 -0700 Subject: [PATCH 131/394] remove --- esphome/automation.py | 45 ++-------------- esphome/components/binary_sensor/__init__.py | 3 +- esphome/components/binary_sensor/filter.h | 15 ------ esphome/components/logger/__init__.py | 10 ++-- esphome/components/sensor/__init__.py | 3 +- esphome/components/sensor/filter.h | 15 ------ esphome/components/text_sensor/__init__.py | 3 +- esphome/components/text_sensor/filter.h | 15 ------ esphome/core/base_automation.h | 25 --------- tests/component_tests/text/test_text.py | 4 +- tests/unit_tests/test_cpp_generator.py | 55 -------------------- 11 files changed, 11 insertions(+), 182 deletions(-) diff --git a/esphome/automation.py b/esphome/automation.py index cfe0af1b59..99def9f273 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -16,12 +16,7 @@ from esphome.const import ( CONF_UPDATE_INTERVAL, ) from esphome.core import ID -from esphome.cpp_generator import ( - LambdaExpression, - MockObj, - MockObjClass, - TemplateArgsType, -) +from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.types import ConfigType from esphome.util import Registry @@ -92,7 +87,6 @@ def validate_potentially_or_condition(value): DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component) LambdaAction = cg.esphome_ns.class_("LambdaAction", Action) -StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action) IfAction = cg.esphome_ns.class_("IfAction", Action) WhileAction = cg.esphome_ns.class_("WhileAction", Action) RepeatAction = cg.esphome_ns.class_("RepeatAction", Action) @@ -103,40 +97,9 @@ ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action) Automation = cg.esphome_ns.class_("Automation") LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition) -StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition) ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component) -def new_lambda_pvariable( - id_obj: ID, - lambda_expr: LambdaExpression, - stateless_class: MockObjClass, - template_arg: cg.TemplateArguments | None = None, -) -> MockObj: - """Create Pvariable for lambda, using stateless class if applicable. - - Combines ID selection and Pvariable creation in one call. For stateless - lambdas (empty capture), uses function pointer instead of std::function. - - Args: - id_obj: The ID object (action_id, condition_id, or filter_id) - lambda_expr: The lambda expression object - stateless_class: The stateless class to use for stateless lambdas - template_arg: Optional template arguments (for actions/conditions) - - Returns: - The created Pvariable - """ - # For stateless lambdas, use function pointer instead of std::function - if lambda_expr.capture == "": - id_obj = id_obj.copy() - id_obj.type = stateless_class - - if template_arg is not None: - return cg.new_Pvariable(id_obj, template_arg, lambda_expr) - return cg.new_Pvariable(id_obj, lambda_expr) - - def validate_automation(extra_schema=None, extra_validators=None, single=False): if extra_schema is None: extra_schema = {} @@ -277,9 +240,7 @@ async def lambda_condition_to_code( args: TemplateArgsType, ) -> MockObj: lambda_ = await cg.process_lambda(config, args, return_type=bool) - return new_lambda_pvariable( - condition_id, lambda_, StatelessLambdaCondition, template_arg - ) + return cg.new_Pvariable(condition_id, template_arg, lambda_) @register_condition( @@ -445,7 +406,7 @@ async def lambda_action_to_code( args: TemplateArgsType, ) -> MockObj: lambda_ = await cg.process_lambda(config, args, return_type=cg.void) - return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg) + return cg.new_Pvariable(action_id, template_arg, lambda_) @register_action( diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 8892b57e6e..26e784a0b8 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -155,7 +155,6 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter) AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component) LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter) -StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter) SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component) _LOGGER = getLogger(__name__) @@ -300,7 +299,7 @@ async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(bool, "x")], return_type=cg.optional.template(bool) ) - return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) + return cg.new_Pvariable(filter_id, lambda_) @register_filter( diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 2d473c3b64..a7eb080feb 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -111,21 +111,6 @@ class LambdaFilter : public Filter { std::function(bool)> f_; }; -/** Optimized lambda filter for stateless lambdas (no capture). - * - * Uses function pointer instead of std::function to reduce memory overhead. - * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). - */ -class StatelessLambdaFilter : public Filter { - public: - explicit StatelessLambdaFilter(optional (*f)(bool)) : f_(f) {} - - optional new_value(bool value) override { return this->f_(value); } - - protected: - optional (*f_)(bool); -}; - class SettleFilter : public Filter, public Component { public: optional new_value(bool value) override; diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 22bf3d2f4c..1d02073d27 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -1,7 +1,7 @@ import re from esphome import automation -from esphome.automation import LambdaAction, StatelessLambdaAction +from esphome.automation import LambdaAction import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant from esphome.components.esp32.const import ( @@ -430,9 +430,7 @@ async def logger_log_action_to_code(config, action_id, template_arg, args): text = str(cg.statement(esp_log(config[CONF_TAG], config[CONF_FORMAT], *args_))) lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) - return automation.new_lambda_pvariable( - action_id, lambda_, StatelessLambdaAction, template_arg - ) + return cg.new_Pvariable(action_id, template_arg, lambda_) @automation.register_action( @@ -457,9 +455,7 @@ async def logger_set_level_to_code(config, action_id, template_arg, args): text = str(cg.statement(logger.set_log_level(level))) lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) - return automation.new_lambda_pvariable( - action_id, lambda_, StatelessLambdaAction, template_arg - ) + return cg.new_Pvariable(action_id, template_arg, lambda_) FILTER_SOURCE_FILES = filter_source_files_from_platform( diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 41ac3516b9..93283e4d47 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -261,7 +261,6 @@ ExponentialMovingAverageFilter = sensor_ns.class_( ) ThrottleAverageFilter = sensor_ns.class_("ThrottleAverageFilter", Filter, cg.Component) LambdaFilter = sensor_ns.class_("LambdaFilter", Filter) -StatelessLambdaFilter = sensor_ns.class_("StatelessLambdaFilter", Filter) OffsetFilter = sensor_ns.class_("OffsetFilter", Filter) MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter) ValueListFilter = sensor_ns.class_("ValueListFilter", Filter) @@ -574,7 +573,7 @@ async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(float, "x")], return_type=cg.optional.template(float) ) - return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) + return cg.new_Pvariable(filter_id, lambda_) DELTA_SCHEMA = cv.Schema( diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 75e28a1efe..ecd55308d1 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -296,21 +296,6 @@ class LambdaFilter : public Filter { lambda_filter_t lambda_filter_; }; -/** Optimized lambda filter for stateless lambdas (no capture). - * - * Uses function pointer instead of std::function to reduce memory overhead. - * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). - */ -class StatelessLambdaFilter : public Filter { - public: - explicit StatelessLambdaFilter(optional (*lambda_filter)(float)) : lambda_filter_(lambda_filter) {} - - optional new_value(float value) override { return this->lambda_filter_(value); } - - protected: - optional (*lambda_filter_)(float); -}; - /// A simple filter that adds `offset` to each value it receives. class OffsetFilter : public Filter { public: diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index adc8a76fcd..7a9e947abd 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -57,7 +57,6 @@ validate_filters = cv.validate_registry("filter", FILTER_REGISTRY) # Filters Filter = text_sensor_ns.class_("Filter") LambdaFilter = text_sensor_ns.class_("LambdaFilter", Filter) -StatelessLambdaFilter = text_sensor_ns.class_("StatelessLambdaFilter", Filter) ToUpperFilter = text_sensor_ns.class_("ToUpperFilter", Filter) ToLowerFilter = text_sensor_ns.class_("ToLowerFilter", Filter) AppendFilter = text_sensor_ns.class_("AppendFilter", Filter) @@ -71,7 +70,7 @@ async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(cg.std_string, "x")], return_type=cg.optional.template(cg.std_string) ) - return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) + return cg.new_Pvariable(filter_id, lambda_) @FILTER_REGISTRY.register("to_upper", ToUpperFilter, {}) diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index 85acac5c8d..c77c221235 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -62,21 +62,6 @@ class LambdaFilter : public Filter { lambda_filter_t lambda_filter_; }; -/** Optimized lambda filter for stateless lambdas (no capture). - * - * Uses function pointer instead of std::function to reduce memory overhead. - * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). - */ -class StatelessLambdaFilter : public Filter { - public: - explicit StatelessLambdaFilter(optional (*lambda_filter)(std::string)) : lambda_filter_(lambda_filter) {} - - optional new_value(std::string value) override { return this->lambda_filter_(value); } - - protected: - optional (*lambda_filter_)(std::string); -}; - /// A simple filter that converts all text to uppercase class ToUpperFilter : public Filter { public: diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 1c60dd1c7a..af8cde971b 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -79,18 +79,6 @@ template class LambdaCondition : public Condition { std::function f_; }; -/// Optimized lambda condition for stateless lambdas (no capture). -/// Uses function pointer instead of std::function to reduce memory overhead. -/// Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). -template class StatelessLambdaCondition : public Condition { - public: - explicit StatelessLambdaCondition(bool (*f)(Ts...)) : f_(f) {} - bool check(Ts... x) override { return this->f_(x...); } - - protected: - bool (*f_)(Ts...); -}; - template class ForCondition : public Condition, public Component { public: explicit ForCondition(Condition<> *condition) : condition_(condition) {} @@ -202,19 +190,6 @@ template class LambdaAction : public Action { std::function f_; }; -/// Optimized lambda action for stateless lambdas (no capture). -/// Uses function pointer instead of std::function to reduce memory overhead. -/// Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). -template class StatelessLambdaAction : public Action { - public: - explicit StatelessLambdaAction(void (*f)(Ts...)) : f_(f) {} - - void play(Ts... x) override { this->f_(x...); } - - protected: - void (*f_)(Ts...); -}; - template class IfAction : public Action { public: explicit IfAction(Condition *condition) : condition_(condition) {} diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 99ddd78ee7..75f1c4b88b 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -58,7 +58,7 @@ def test_text_config_value_mode_set(generate_main): def test_text_config_lamda_is_set(generate_main): """ - Test if lambda is set for lambda mode (optimized with stateless lambda) + Test if lambda is set for lambda mode """ # Given @@ -66,5 +66,5 @@ def test_text_config_lamda_is_set(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert "it_4->set_template([]() -> esphome::optional {" in main_cpp + assert "it_4->set_template([=]() -> esphome::optional {" in main_cpp assert 'return std::string{"Hello"};' in main_cpp diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 2c9f760c8e..95633ca0c6 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -173,61 +173,6 @@ class TestLambdaExpression: "}" ) - def test_str__stateless_no_return(self): - """Test stateless lambda (empty capture) generates correctly""" - target = cg.LambdaExpression( - ('ESP_LOGD("main", "Test message");',), - (), # No parameters - "", # Empty capture (stateless) - ) - - actual = str(target) - - assert actual == ('[]() {\n ESP_LOGD("main", "Test message");\n}') - - def test_str__stateless_with_return(self): - """Test stateless lambda with return type generates correctly""" - target = cg.LambdaExpression( - ("return global_value > 0;",), - (), # No parameters - "", # Empty capture (stateless) - bool, # Return type - ) - - actual = str(target) - - assert actual == ("[]() -> bool {\n return global_value > 0;\n}") - - def test_str__stateless_with_params(self): - """Test stateless lambda with parameters generates correctly""" - target = cg.LambdaExpression( - ("return foo + bar;",), - ((int, "foo"), (float, "bar")), - "", # Empty capture (stateless) - float, - ) - - actual = str(target) - - assert actual == ( - "[](int32_t foo, float bar) -> float {\n return foo + bar;\n}" - ) - - def test_str__with_capture(self): - """Test lambda with capture generates correctly""" - target = cg.LambdaExpression( - ("return captured_var + x;",), - ((int, "x"),), - "captured_var", # Has capture (not stateless) - int, - ) - - actual = str(target) - - assert actual == ( - "[captured_var](int32_t x) -> int32_t {\n return captured_var + x;\n}" - ) - class TestLiterals: @pytest.mark.parametrize( From 0bbe32683054ed933e7b222c6e38ae59f819fa81 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 11:51:42 -0700 Subject: [PATCH 132/394] preen --- esphome/core/automation.h | 71 +++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index fe2daa4f80..eb8c9316ba 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -46,24 +46,36 @@ template class TemplatableValue { // Copy constructor TemplatableValue(const TemplatableValue &other) : type_(other.type_) { - if (type_ == VALUE) { - new (&this->value_) T(other.value_); - } else if (type_ == LAMBDA) { - this->f_ = new std::function(*other.f_); - } else if (type_ == STATELESS_LAMBDA) { - this->stateless_f_ = other.stateless_f_; + switch (type_) { + case VALUE: + new (&this->value_) T(other.value_); + break; + case LAMBDA: + this->f_ = new std::function(*other.f_); + break; + case STATELESS_LAMBDA: + this->stateless_f_ = other.stateless_f_; + break; + case NONE: + break; } } // Move constructor TemplatableValue(TemplatableValue &&other) noexcept : type_(other.type_) { - if (type_ == VALUE) { - new (&this->value_) T(std::move(other.value_)); - } else if (type_ == LAMBDA) { - this->f_ = other.f_; - other.f_ = nullptr; - } else if (type_ == STATELESS_LAMBDA) { - this->stateless_f_ = other.stateless_f_; + switch (type_) { + case VALUE: + new (&this->value_) T(std::move(other.value_)); + break; + case LAMBDA: + this->f_ = other.f_; + other.f_ = nullptr; + break; + case STATELESS_LAMBDA: + this->stateless_f_ = other.stateless_f_; + break; + case NONE: + break; } other.type_ = NONE; } @@ -86,25 +98,34 @@ template class TemplatableValue { } ~TemplatableValue() { - if (type_ == VALUE) { - this->value_.~T(); - } else if (type_ == LAMBDA) { - delete this->f_; + switch (type_) { + case VALUE: + this->value_.~T(); + break; + case LAMBDA: + delete this->f_; + break; + case STATELESS_LAMBDA: + case NONE: + // No cleanup needed (function pointer or empty, not heap-allocated) + break; } - // STATELESS_LAMBDA needs no cleanup (function pointer, not heap-allocated) } bool has_value() { return this->type_ != NONE; } T value(X... x) { - if (this->type_ == STATELESS_LAMBDA) { - return this->stateless_f_(x...); // Direct function pointer call + switch (this->type_) { + case STATELESS_LAMBDA: + return this->stateless_f_(x...); // Direct function pointer call + case LAMBDA: + return (*this->f_)(x...); // std::function call + case VALUE: + return this->value_; + case NONE: + default: + return T{}; } - if (this->type_ == LAMBDA) { - return (*this->f_)(x...); // std::function call - } - // return value also when none - return this->type_ == VALUE ? this->value_ : T{}; } optional optional_value(X... x) { From b68d030f5a9a2648f63deae1d2f53f4951e82b35 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 11:59:12 -0700 Subject: [PATCH 133/394] update tests --- tests/component_tests/text/test_text.py | 4 +- tests/unit_tests/test_cpp_generator.py | 55 +++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 75f1c4b88b..99ddd78ee7 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -58,7 +58,7 @@ def test_text_config_value_mode_set(generate_main): def test_text_config_lamda_is_set(generate_main): """ - Test if lambda is set for lambda mode + Test if lambda is set for lambda mode (optimized with stateless lambda) """ # Given @@ -66,5 +66,5 @@ def test_text_config_lamda_is_set(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert "it_4->set_template([=]() -> esphome::optional {" in main_cpp + assert "it_4->set_template([]() -> esphome::optional {" in main_cpp assert 'return std::string{"Hello"};' in main_cpp diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 95633ca0c6..2c9f760c8e 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -173,6 +173,61 @@ class TestLambdaExpression: "}" ) + def test_str__stateless_no_return(self): + """Test stateless lambda (empty capture) generates correctly""" + target = cg.LambdaExpression( + ('ESP_LOGD("main", "Test message");',), + (), # No parameters + "", # Empty capture (stateless) + ) + + actual = str(target) + + assert actual == ('[]() {\n ESP_LOGD("main", "Test message");\n}') + + def test_str__stateless_with_return(self): + """Test stateless lambda with return type generates correctly""" + target = cg.LambdaExpression( + ("return global_value > 0;",), + (), # No parameters + "", # Empty capture (stateless) + bool, # Return type + ) + + actual = str(target) + + assert actual == ("[]() -> bool {\n return global_value > 0;\n}") + + def test_str__stateless_with_params(self): + """Test stateless lambda with parameters generates correctly""" + target = cg.LambdaExpression( + ("return foo + bar;",), + ((int, "foo"), (float, "bar")), + "", # Empty capture (stateless) + float, + ) + + actual = str(target) + + assert actual == ( + "[](int32_t foo, float bar) -> float {\n return foo + bar;\n}" + ) + + def test_str__with_capture(self): + """Test lambda with capture generates correctly""" + target = cg.LambdaExpression( + ("return captured_var + x;",), + ((int, "x"),), + "captured_var", # Has capture (not stateless) + int, + ) + + actual = str(target) + + assert actual == ( + "[captured_var](int32_t x) -> int32_t {\n return captured_var + x;\n}" + ) + class TestLiterals: @pytest.mark.parametrize( From 48b45ba4391ed377ce83c31ce8e314283e011c0c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 12:01:54 -0700 Subject: [PATCH 134/394] we have c++20 --- esphome/core/automation.h | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index eb8c9316ba..82dce303d2 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -27,20 +27,21 @@ template class TemplatableValue { public: TemplatableValue() : type_(NONE) {} - template::value, int> = 0> TemplatableValue(F value) : type_(VALUE) { + template + requires(!std::invocable) TemplatableValue(F value) : type_(VALUE) { new (&this->value_) T(std::move(value)); } // For stateless lambdas (convertible to function pointer): use function pointer - template::value && std::is_convertible::value, int> = 0> - TemplatableValue(F f) : type_(STATELESS_LAMBDA) { + template + requires std::invocable && std::convertible_to TemplatableValue(F f) + : type_(STATELESS_LAMBDA) { this->stateless_f_ = f; // Implicit conversion to function pointer } // For stateful lambdas (not convertible to function pointer): use std::function - template::value && !std::is_convertible::value, int> = 0> - TemplatableValue(F f) : type_(LAMBDA) { + template + requires std::invocable &&(!std::convertible_to) TemplatableValue(F f) : type_(LAMBDA) { this->f_ = new std::function(std::move(f)); } From 1652ea8b97d939e7ce5ab3cda335adc74780f2ae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 12:14:01 -0700 Subject: [PATCH 135/394] overkill --- esphome/core/automation.h | 54 +++++++++++++-------------------------- 1 file changed, 18 insertions(+), 36 deletions(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 82dce303d2..d357c6f58e 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -47,36 +47,24 @@ template class TemplatableValue { // Copy constructor TemplatableValue(const TemplatableValue &other) : type_(other.type_) { - switch (type_) { - case VALUE: - new (&this->value_) T(other.value_); - break; - case LAMBDA: - this->f_ = new std::function(*other.f_); - break; - case STATELESS_LAMBDA: - this->stateless_f_ = other.stateless_f_; - break; - case NONE: - break; + if (type_ == VALUE) { + new (&this->value_) T(other.value_); + } else if (type_ == LAMBDA) { + this->f_ = new std::function(*other.f_); + } else if (type_ == STATELESS_LAMBDA) { + this->stateless_f_ = other.stateless_f_; } } // Move constructor TemplatableValue(TemplatableValue &&other) noexcept : type_(other.type_) { - switch (type_) { - case VALUE: - new (&this->value_) T(std::move(other.value_)); - break; - case LAMBDA: - this->f_ = other.f_; - other.f_ = nullptr; - break; - case STATELESS_LAMBDA: - this->stateless_f_ = other.stateless_f_; - break; - case NONE: - break; + if (type_ == VALUE) { + new (&this->value_) T(std::move(other.value_)); + } else if (type_ == LAMBDA) { + this->f_ = other.f_; + other.f_ = nullptr; + } else if (type_ == STATELESS_LAMBDA) { + this->stateless_f_ = other.stateless_f_; } other.type_ = NONE; } @@ -99,18 +87,12 @@ template class TemplatableValue { } ~TemplatableValue() { - switch (type_) { - case VALUE: - this->value_.~T(); - break; - case LAMBDA: - delete this->f_; - break; - case STATELESS_LAMBDA: - case NONE: - // No cleanup needed (function pointer or empty, not heap-allocated) - break; + if (type_ == VALUE) { + this->value_.~T(); + } else if (type_ == LAMBDA) { + delete this->f_; } + // STATELESS_LAMBDA/NONE: no cleanup needed (function pointer or empty, not heap-allocated) } bool has_value() { return this->type_ != NONE; } From 35b595924954048aaf6b7722177a055603712dfd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 12:19:39 -0700 Subject: [PATCH 136/394] Revert "remove" This reverts commit 077bd624f08712355b6998b8269ed153272e88e5. --- esphome/automation.py | 45 ++++++++++++++++++-- esphome/components/binary_sensor/__init__.py | 3 +- esphome/components/binary_sensor/filter.h | 15 +++++++ esphome/components/logger/__init__.py | 10 +++-- esphome/components/sensor/__init__.py | 3 +- esphome/components/sensor/filter.h | 15 +++++++ esphome/components/text_sensor/__init__.py | 3 +- esphome/components/text_sensor/filter.h | 15 +++++++ esphome/core/base_automation.h | 25 +++++++++++ 9 files changed, 125 insertions(+), 9 deletions(-) diff --git a/esphome/automation.py b/esphome/automation.py index 99def9f273..cfe0af1b59 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -16,7 +16,12 @@ from esphome.const import ( CONF_UPDATE_INTERVAL, ) from esphome.core import ID -from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType +from esphome.cpp_generator import ( + LambdaExpression, + MockObj, + MockObjClass, + TemplateArgsType, +) from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.types import ConfigType from esphome.util import Registry @@ -87,6 +92,7 @@ def validate_potentially_or_condition(value): DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component) LambdaAction = cg.esphome_ns.class_("LambdaAction", Action) +StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action) IfAction = cg.esphome_ns.class_("IfAction", Action) WhileAction = cg.esphome_ns.class_("WhileAction", Action) RepeatAction = cg.esphome_ns.class_("RepeatAction", Action) @@ -97,9 +103,40 @@ ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action) Automation = cg.esphome_ns.class_("Automation") LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition) +StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition) ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component) +def new_lambda_pvariable( + id_obj: ID, + lambda_expr: LambdaExpression, + stateless_class: MockObjClass, + template_arg: cg.TemplateArguments | None = None, +) -> MockObj: + """Create Pvariable for lambda, using stateless class if applicable. + + Combines ID selection and Pvariable creation in one call. For stateless + lambdas (empty capture), uses function pointer instead of std::function. + + Args: + id_obj: The ID object (action_id, condition_id, or filter_id) + lambda_expr: The lambda expression object + stateless_class: The stateless class to use for stateless lambdas + template_arg: Optional template arguments (for actions/conditions) + + Returns: + The created Pvariable + """ + # For stateless lambdas, use function pointer instead of std::function + if lambda_expr.capture == "": + id_obj = id_obj.copy() + id_obj.type = stateless_class + + if template_arg is not None: + return cg.new_Pvariable(id_obj, template_arg, lambda_expr) + return cg.new_Pvariable(id_obj, lambda_expr) + + def validate_automation(extra_schema=None, extra_validators=None, single=False): if extra_schema is None: extra_schema = {} @@ -240,7 +277,9 @@ async def lambda_condition_to_code( args: TemplateArgsType, ) -> MockObj: lambda_ = await cg.process_lambda(config, args, return_type=bool) - return cg.new_Pvariable(condition_id, template_arg, lambda_) + return new_lambda_pvariable( + condition_id, lambda_, StatelessLambdaCondition, template_arg + ) @register_condition( @@ -406,7 +445,7 @@ async def lambda_action_to_code( args: TemplateArgsType, ) -> MockObj: lambda_ = await cg.process_lambda(config, args, return_type=cg.void) - return cg.new_Pvariable(action_id, template_arg, lambda_) + return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg) @register_action( diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 26e784a0b8..8892b57e6e 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -155,6 +155,7 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter) AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component) LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter) +StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter) SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component) _LOGGER = getLogger(__name__) @@ -299,7 +300,7 @@ async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(bool, "x")], return_type=cg.optional.template(bool) ) - return cg.new_Pvariable(filter_id, lambda_) + return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) @register_filter( diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index a7eb080feb..2d473c3b64 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -111,6 +111,21 @@ class LambdaFilter : public Filter { std::function(bool)> f_; }; +/** Optimized lambda filter for stateless lambdas (no capture). + * + * Uses function pointer instead of std::function to reduce memory overhead. + * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). + */ +class StatelessLambdaFilter : public Filter { + public: + explicit StatelessLambdaFilter(optional (*f)(bool)) : f_(f) {} + + optional new_value(bool value) override { return this->f_(value); } + + protected: + optional (*f_)(bool); +}; + class SettleFilter : public Filter, public Component { public: optional new_value(bool value) override; diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 1d02073d27..22bf3d2f4c 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -1,7 +1,7 @@ import re from esphome import automation -from esphome.automation import LambdaAction +from esphome.automation import LambdaAction, StatelessLambdaAction import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant from esphome.components.esp32.const import ( @@ -430,7 +430,9 @@ async def logger_log_action_to_code(config, action_id, template_arg, args): text = str(cg.statement(esp_log(config[CONF_TAG], config[CONF_FORMAT], *args_))) lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) - return cg.new_Pvariable(action_id, template_arg, lambda_) + return automation.new_lambda_pvariable( + action_id, lambda_, StatelessLambdaAction, template_arg + ) @automation.register_action( @@ -455,7 +457,9 @@ async def logger_set_level_to_code(config, action_id, template_arg, args): text = str(cg.statement(logger.set_log_level(level))) lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) - return cg.new_Pvariable(action_id, template_arg, lambda_) + return automation.new_lambda_pvariable( + action_id, lambda_, StatelessLambdaAction, template_arg + ) FILTER_SOURCE_FILES = filter_source_files_from_platform( diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 93283e4d47..41ac3516b9 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -261,6 +261,7 @@ ExponentialMovingAverageFilter = sensor_ns.class_( ) ThrottleAverageFilter = sensor_ns.class_("ThrottleAverageFilter", Filter, cg.Component) LambdaFilter = sensor_ns.class_("LambdaFilter", Filter) +StatelessLambdaFilter = sensor_ns.class_("StatelessLambdaFilter", Filter) OffsetFilter = sensor_ns.class_("OffsetFilter", Filter) MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter) ValueListFilter = sensor_ns.class_("ValueListFilter", Filter) @@ -573,7 +574,7 @@ async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(float, "x")], return_type=cg.optional.template(float) ) - return cg.new_Pvariable(filter_id, lambda_) + return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) DELTA_SCHEMA = cv.Schema( diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index ecd55308d1..75e28a1efe 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -296,6 +296,21 @@ class LambdaFilter : public Filter { lambda_filter_t lambda_filter_; }; +/** Optimized lambda filter for stateless lambdas (no capture). + * + * Uses function pointer instead of std::function to reduce memory overhead. + * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). + */ +class StatelessLambdaFilter : public Filter { + public: + explicit StatelessLambdaFilter(optional (*lambda_filter)(float)) : lambda_filter_(lambda_filter) {} + + optional new_value(float value) override { return this->lambda_filter_(value); } + + protected: + optional (*lambda_filter_)(float); +}; + /// A simple filter that adds `offset` to each value it receives. class OffsetFilter : public Filter { public: diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 7a9e947abd..adc8a76fcd 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -57,6 +57,7 @@ validate_filters = cv.validate_registry("filter", FILTER_REGISTRY) # Filters Filter = text_sensor_ns.class_("Filter") LambdaFilter = text_sensor_ns.class_("LambdaFilter", Filter) +StatelessLambdaFilter = text_sensor_ns.class_("StatelessLambdaFilter", Filter) ToUpperFilter = text_sensor_ns.class_("ToUpperFilter", Filter) ToLowerFilter = text_sensor_ns.class_("ToLowerFilter", Filter) AppendFilter = text_sensor_ns.class_("AppendFilter", Filter) @@ -70,7 +71,7 @@ async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(cg.std_string, "x")], return_type=cg.optional.template(cg.std_string) ) - return cg.new_Pvariable(filter_id, lambda_) + return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) @FILTER_REGISTRY.register("to_upper", ToUpperFilter, {}) diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index c77c221235..85acac5c8d 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -62,6 +62,21 @@ class LambdaFilter : public Filter { lambda_filter_t lambda_filter_; }; +/** Optimized lambda filter for stateless lambdas (no capture). + * + * Uses function pointer instead of std::function to reduce memory overhead. + * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). + */ +class StatelessLambdaFilter : public Filter { + public: + explicit StatelessLambdaFilter(optional (*lambda_filter)(std::string)) : lambda_filter_(lambda_filter) {} + + optional new_value(std::string value) override { return this->lambda_filter_(value); } + + protected: + optional (*lambda_filter_)(std::string); +}; + /// A simple filter that converts all text to uppercase class ToUpperFilter : public Filter { public: diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index af8cde971b..1c60dd1c7a 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -79,6 +79,18 @@ template class LambdaCondition : public Condition { std::function f_; }; +/// Optimized lambda condition for stateless lambdas (no capture). +/// Uses function pointer instead of std::function to reduce memory overhead. +/// Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). +template class StatelessLambdaCondition : public Condition { + public: + explicit StatelessLambdaCondition(bool (*f)(Ts...)) : f_(f) {} + bool check(Ts... x) override { return this->f_(x...); } + + protected: + bool (*f_)(Ts...); +}; + template class ForCondition : public Condition, public Component { public: explicit ForCondition(Condition<> *condition) : condition_(condition) {} @@ -190,6 +202,19 @@ template class LambdaAction : public Action { std::function f_; }; +/// Optimized lambda action for stateless lambdas (no capture). +/// Uses function pointer instead of std::function to reduce memory overhead. +/// Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). +template class StatelessLambdaAction : public Action { + public: + explicit StatelessLambdaAction(void (*f)(Ts...)) : f_(f) {} + + void play(Ts... x) override { this->f_(x...); } + + protected: + void (*f_)(Ts...); +}; + template class IfAction : public Action { public: explicit IfAction(Condition *condition) : condition_(condition) {} From 561c89143240b647699a2e92f19afb84362d8477 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 12:23:48 -0700 Subject: [PATCH 137/394] cleanup --- esphome/core/automation.h | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index d357c6f58e..83f0941bac 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -27,21 +27,20 @@ template class TemplatableValue { public: TemplatableValue() : type_(NONE) {} - template - requires(!std::invocable) TemplatableValue(F value) : type_(VALUE) { + template TemplatableValue(F value) requires(!std::invocable) : type_(VALUE) { new (&this->value_) T(std::move(value)); } // For stateless lambdas (convertible to function pointer): use function pointer template - requires std::invocable && std::convertible_to TemplatableValue(F f) + TemplatableValue(F f) requires std::invocable && std::convertible_to : type_(STATELESS_LAMBDA) { this->stateless_f_ = f; // Implicit conversion to function pointer } // For stateful lambdas (not convertible to function pointer): use std::function template - requires std::invocable &&(!std::convertible_to) TemplatableValue(F f) : type_(LAMBDA) { + TemplatableValue(F f) requires std::invocable && !std::convertible_to : type_(LAMBDA) { this->f_ = new std::function(std::move(f)); } From 4967f405513e055ca9df5cda04480c912b3911b1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 12:28:09 -0700 Subject: [PATCH 138/394] cleanup --- esphome/core/automation.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 83f0941bac..5787373cbc 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -40,7 +40,7 @@ template class TemplatableValue { // For stateful lambdas (not convertible to function pointer): use std::function template - TemplatableValue(F f) requires std::invocable && !std::convertible_to : type_(LAMBDA) { + TemplatableValue(F f) requires std::invocable &&(!std::convertible_to) : type_(LAMBDA) { this->f_ = new std::function(std::move(f)); } From 17d875c8e77d5049aa534ef0489843e0a546bf73 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 19:39:56 -0500 Subject: [PATCH 139/394] [template] Optimize all template platforms to use function pointers for stateless lambdas --- .../template/binary_sensor/__init__.py | 8 ++- .../binary_sensor/template_binary_sensor.cpp | 4 +- .../binary_sensor/template_binary_sensor.h | 4 +- .../template/cover/template_cover.cpp | 4 +- .../template/cover/template_cover.h | 8 +-- .../template/datetime/template_date.h | 4 +- .../template/datetime/template_datetime.h | 4 +- .../template/datetime/template_time.h | 4 +- .../template/lock/template_lock.cpp | 2 +- .../components/template/lock/template_lock.h | 4 +- .../template/number/template_number.h | 4 +- .../template/select/template_select.h | 4 +- .../template/sensor/template_sensor.cpp | 2 +- .../template/sensor/template_sensor.h | 4 +- .../template/switch/template_switch.cpp | 2 +- .../template/switch/template_switch.h | 4 +- .../components/template/text/template_text.h | 4 +- .../text_sensor/template_text_sensor.cpp | 2 +- .../text_sensor/template_text_sensor.h | 4 +- .../template/valve/template_valve.cpp | 2 +- .../template/valve/template_valve.h | 4 +- esphome/cpp_generator.py | 8 +++ tests/component_tests/text/test_text.py | 5 +- tests/unit_tests/test_cpp_generator.py | 55 +++++++++++++++++++ 24 files changed, 110 insertions(+), 40 deletions(-) diff --git a/esphome/components/template/binary_sensor/__init__.py b/esphome/components/template/binary_sensor/__init__.py index c93876380d..9d4208dcca 100644 --- a/esphome/components/template/binary_sensor/__init__.py +++ b/esphome/components/template/binary_sensor/__init__.py @@ -38,8 +38,14 @@ async def to_code(config): condition = await automation.build_condition( condition, cg.TemplateArguments(), [] ) + # Generate a stateless lambda that calls condition.check() + # capture="" is safe because condition is a global variable in generated C++ code + # and doesn't need to be captured. This allows implicit conversion to function pointer. template_ = LambdaExpression( - f"return {condition.check()};", [], return_type=cg.optional.template(bool) + f"return {condition.check()};", + [], + return_type=cg.optional.template(bool), + capture="", ) cg.add(var.set_template(template_)) diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.cpp b/esphome/components/template/binary_sensor/template_binary_sensor.cpp index d1fb618695..8543dff4dc 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.cpp +++ b/esphome/components/template/binary_sensor/template_binary_sensor.cpp @@ -9,10 +9,10 @@ static const char *const TAG = "template.binary_sensor"; void TemplateBinarySensor::setup() { this->loop(); } void TemplateBinarySensor::loop() { - if (this->f_ == nullptr) + if (!this->f_.has_value()) return; - auto s = this->f_(); + auto s = (*this->f_)(); if (s.has_value()) { this->publish_state(*s); } diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.h b/esphome/components/template/binary_sensor/template_binary_sensor.h index 5e5624d82e..2e0b216eb4 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.h +++ b/esphome/components/template/binary_sensor/template_binary_sensor.h @@ -8,7 +8,7 @@ namespace template_ { class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void loop() override; @@ -17,7 +17,7 @@ class TemplateBinarySensor : public Component, public binary_sensor::BinarySenso float get_setup_priority() const override { return setup_priority::HARDWARE; } protected: - std::function()> f_{nullptr}; + optional (*)()> f_; }; } // namespace template_ diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index 84c687536e..bed3931e78 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -63,7 +63,7 @@ void TemplateCover::loop() { } void TemplateCover::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void TemplateCover::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } -void TemplateCover::set_state_lambda(std::function()> &&f) { this->state_f_ = f; } +void TemplateCover::set_state_lambda(optional (*f)()) { this->state_f_ = f; } float TemplateCover::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; } Trigger<> *TemplateCover::get_close_trigger() const { return this->close_trigger_; } @@ -124,7 +124,7 @@ CoverTraits TemplateCover::get_traits() { } Trigger *TemplateCover::get_position_trigger() const { return this->position_trigger_; } Trigger *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; } -void TemplateCover::set_tilt_lambda(std::function()> &&tilt_f) { this->tilt_f_ = tilt_f; } +void TemplateCover::set_tilt_lambda(optional (*tilt_f)()) { this->tilt_f_ = tilt_f; } void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } void TemplateCover::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index 958c94b0a6..ed1ebf4e43 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -17,7 +17,7 @@ class TemplateCover : public cover::Cover, public Component { public: TemplateCover(); - void set_state_lambda(std::function()> &&f); + void set_state_lambda(optional (*f)()); Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; @@ -26,7 +26,7 @@ class TemplateCover : public cover::Cover, public Component { Trigger *get_tilt_trigger() const; void set_optimistic(bool optimistic); void set_assumed_state(bool assumed_state); - void set_tilt_lambda(std::function()> &&tilt_f); + void set_tilt_lambda(optional (*tilt_f)()); void set_has_stop(bool has_stop); void set_has_position(bool has_position); void set_has_tilt(bool has_tilt); @@ -45,8 +45,8 @@ class TemplateCover : public cover::Cover, public Component { void stop_prev_trigger_(); TemplateCoverRestoreMode restore_mode_{COVER_RESTORE}; - optional()>> state_f_; - optional()>> tilt_f_; + optional (*)()> state_f_; + optional (*)()> tilt_f_; bool assumed_state_{false}; bool optimistic_{false}; Trigger<> *open_trigger_; diff --git a/esphome/components/template/datetime/template_date.h b/esphome/components/template/datetime/template_date.h index 185c7ed49d..2a0967fc94 100644 --- a/esphome/components/template/datetime/template_date.h +++ b/esphome/components/template/datetime/template_date.h @@ -15,7 +15,7 @@ namespace template_ { class TemplateDate : public datetime::DateEntity, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void update() override; @@ -35,7 +35,7 @@ class TemplateDate : public datetime::DateEntity, public PollingComponent { ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + optional (*)()> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/datetime/template_datetime.h b/esphome/components/template/datetime/template_datetime.h index ef80ded89a..d917015b67 100644 --- a/esphome/components/template/datetime/template_datetime.h +++ b/esphome/components/template/datetime/template_datetime.h @@ -15,7 +15,7 @@ namespace template_ { class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void update() override; @@ -35,7 +35,7 @@ class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponen ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + optional (*)()> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/datetime/template_time.h b/esphome/components/template/datetime/template_time.h index 4a7c0098ec..2f05ba0737 100644 --- a/esphome/components/template/datetime/template_time.h +++ b/esphome/components/template/datetime/template_time.h @@ -15,7 +15,7 @@ namespace template_ { class TemplateTime : public datetime::TimeEntity, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void update() override; @@ -35,7 +35,7 @@ class TemplateTime : public datetime::TimeEntity, public PollingComponent { ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + optional (*)()> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/lock/template_lock.cpp b/esphome/components/template/lock/template_lock.cpp index 87ba1046eb..c2e227c26d 100644 --- a/esphome/components/template/lock/template_lock.cpp +++ b/esphome/components/template/lock/template_lock.cpp @@ -45,7 +45,7 @@ void TemplateLock::open_latch() { this->open_trigger_->trigger(); } void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } -void TemplateLock::set_state_lambda(std::function()> &&f) { this->f_ = f; } +void TemplateLock::set_state_lambda(optional (*f)()) { this->f_ = f; } float TemplateLock::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateLock::get_lock_trigger() const { return this->lock_trigger_; } Trigger<> *TemplateLock::get_unlock_trigger() const { return this->unlock_trigger_; } diff --git a/esphome/components/template/lock/template_lock.h b/esphome/components/template/lock/template_lock.h index 4f798eca81..428744a66f 100644 --- a/esphome/components/template/lock/template_lock.h +++ b/esphome/components/template/lock/template_lock.h @@ -13,7 +13,7 @@ class TemplateLock : public lock::Lock, public Component { void dump_config() override; - void set_state_lambda(std::function()> &&f); + void set_state_lambda(optional (*f)()); Trigger<> *get_lock_trigger() const; Trigger<> *get_unlock_trigger() const; Trigger<> *get_open_trigger() const; @@ -26,7 +26,7 @@ class TemplateLock : public lock::Lock, public Component { void control(const lock::LockCall &call) override; void open_latch() override; - optional()>> f_; + optional (*)()> f_; bool optimistic_{false}; Trigger<> *lock_trigger_; Trigger<> *unlock_trigger_; diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h index 9a82e44339..e77b181d25 100644 --- a/esphome/components/template/number/template_number.h +++ b/esphome/components/template/number/template_number.h @@ -10,7 +10,7 @@ namespace template_ { class TemplateNumber : public number::Number, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void update() override; @@ -28,7 +28,7 @@ class TemplateNumber : public number::Number, public PollingComponent { float initial_value_{NAN}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + optional (*)()> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 2f00765c3d..c1b348b26a 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -10,7 +10,7 @@ namespace template_ { class TemplateSelect : public select::Select, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void update() override; @@ -28,7 +28,7 @@ class TemplateSelect : public select::Select, public PollingComponent { std::string initial_option_; bool restore_value_ = false; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + optional (*)()> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/sensor/template_sensor.cpp b/esphome/components/template/sensor/template_sensor.cpp index f2d0e7363e..65f2417670 100644 --- a/esphome/components/template/sensor/template_sensor.cpp +++ b/esphome/components/template/sensor/template_sensor.cpp @@ -17,7 +17,7 @@ void TemplateSensor::update() { } } float TemplateSensor::get_setup_priority() const { return setup_priority::HARDWARE; } -void TemplateSensor::set_template(std::function()> &&f) { this->f_ = f; } +void TemplateSensor::set_template(optional (*f)()) { this->f_ = f; } void TemplateSensor::dump_config() { LOG_SENSOR("", "Template Sensor", this); LOG_UPDATE_INTERVAL(this); diff --git a/esphome/components/template/sensor/template_sensor.h b/esphome/components/template/sensor/template_sensor.h index 2630cb0b14..369313d607 100644 --- a/esphome/components/template/sensor/template_sensor.h +++ b/esphome/components/template/sensor/template_sensor.h @@ -8,7 +8,7 @@ namespace template_ { class TemplateSensor : public sensor::Sensor, public PollingComponent { public: - void set_template(std::function()> &&f); + void set_template(optional (*f)()); void update() override; @@ -17,7 +17,7 @@ class TemplateSensor : public sensor::Sensor, public PollingComponent { float get_setup_priority() const override; protected: - optional()>> f_; + optional (*)()> f_; }; } // namespace template_ diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index fa236f6364..5aaf514b2a 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -35,7 +35,7 @@ void TemplateSwitch::write_state(bool state) { } void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } bool TemplateSwitch::assumed_state() { return this->assumed_state_; } -void TemplateSwitch::set_state_lambda(std::function()> &&f) { this->f_ = f; } +void TemplateSwitch::set_state_lambda(optional (*f)()) { this->f_ = f; } float TemplateSwitch::get_setup_priority() const { return setup_priority::HARDWARE - 2.0f; } Trigger<> *TemplateSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } Trigger<> *TemplateSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index bfe9ac25d6..0fba66b9bd 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -14,7 +14,7 @@ class TemplateSwitch : public switch_::Switch, public Component { void setup() override; void dump_config() override; - void set_state_lambda(std::function()> &&f); + void set_state_lambda(optional (*f)()); Trigger<> *get_turn_on_trigger() const; Trigger<> *get_turn_off_trigger() const; void set_optimistic(bool optimistic); @@ -28,7 +28,7 @@ class TemplateSwitch : public switch_::Switch, public Component { void write_state(bool state) override; - optional()>> f_; + optional (*)()> f_; bool optimistic_{false}; bool assumed_state_{false}; Trigger<> *turn_on_trigger_; diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index bcfc54a2ba..6c17d2016a 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -61,7 +61,7 @@ template class TextSaver : public TemplateTextSaverBase { class TemplateText : public text::Text, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void update() override; @@ -78,7 +78,7 @@ class TemplateText : public text::Text, public PollingComponent { bool optimistic_ = false; std::string initial_value_; Trigger *set_trigger_ = new Trigger(); - optional()>> f_{nullptr}; + optional (*)()> f_{nullptr}; TemplateTextSaverBase *pref_ = nullptr; }; diff --git a/esphome/components/template/text_sensor/template_text_sensor.cpp b/esphome/components/template/text_sensor/template_text_sensor.cpp index 885ad47bbf..2b0297d62f 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.cpp +++ b/esphome/components/template/text_sensor/template_text_sensor.cpp @@ -16,7 +16,7 @@ void TemplateTextSensor::update() { } } float TemplateTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } -void TemplateTextSensor::set_template(std::function()> &&f) { this->f_ = f; } +void TemplateTextSensor::set_template(optional (*f)()) { this->f_ = f; } void TemplateTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Template Sensor", this); } } // namespace template_ diff --git a/esphome/components/template/text_sensor/template_text_sensor.h b/esphome/components/template/text_sensor/template_text_sensor.h index 07a2bd96fc..48e40c2493 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.h +++ b/esphome/components/template/text_sensor/template_text_sensor.h @@ -9,7 +9,7 @@ namespace template_ { class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent { public: - void set_template(std::function()> &&f); + void set_template(optional (*f)()); void update() override; @@ -18,7 +18,7 @@ class TemplateTextSensor : public text_sensor::TextSensor, public PollingCompone void dump_config() override; protected: - optional()>> f_{}; + optional (*)()> f_{}; }; } // namespace template_ diff --git a/esphome/components/template/valve/template_valve.cpp b/esphome/components/template/valve/template_valve.cpp index 5fa14a2de7..b27cc00968 100644 --- a/esphome/components/template/valve/template_valve.cpp +++ b/esphome/components/template/valve/template_valve.cpp @@ -55,7 +55,7 @@ void TemplateValve::loop() { void TemplateValve::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void TemplateValve::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } -void TemplateValve::set_state_lambda(std::function()> &&f) { this->state_f_ = f; } +void TemplateValve::set_state_lambda(optional (*f)()) { this->state_f_ = f; } float TemplateValve::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateValve::get_open_trigger() const { return this->open_trigger_; } diff --git a/esphome/components/template/valve/template_valve.h b/esphome/components/template/valve/template_valve.h index 5e3fb6aff3..92c32f3487 100644 --- a/esphome/components/template/valve/template_valve.h +++ b/esphome/components/template/valve/template_valve.h @@ -17,7 +17,7 @@ class TemplateValve : public valve::Valve, public Component { public: TemplateValve(); - void set_state_lambda(std::function()> &&f); + void set_state_lambda(optional (*f)()); Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; @@ -42,7 +42,7 @@ class TemplateValve : public valve::Valve, public Component { void stop_prev_trigger_(); TemplateValveRestoreMode restore_mode_{VALVE_NO_RESTORE}; - optional()>> state_f_; + optional (*)()> state_f_; bool assumed_state_{false}; bool optimistic_{false}; Trigger<> *open_trigger_; diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index b2022c7ae6..a2da424e5a 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -198,6 +198,8 @@ class LambdaExpression(Expression): self.return_type = safe_exp(return_type) if return_type is not None else None def __str__(self): + # Stateless lambdas (empty capture) implicitly convert to function pointers + # when assigned to function pointer types - no unary + needed cpp = f"[{self.capture}]({self.parameters})" if self.return_type is not None: cpp += f" -> {self.return_type}" @@ -700,6 +702,12 @@ async def process_lambda( parts[i * 3 + 1] = var parts[i * 3 + 2] = "" + # All id() references are global variables in generated C++ code. + # Global variables should not be captured - they're accessible everywhere. + # Use empty capture instead of capture-by-value. + if capture == "=": + capture = "" + if isinstance(value, ESPHomeDataBase) and value.esp_range is not None: location = value.esp_range.start_mark location.line += value.content_offset diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 75f1c4b88b..1cc31a288b 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -58,7 +58,7 @@ def test_text_config_value_mode_set(generate_main): def test_text_config_lamda_is_set(generate_main): """ - Test if lambda is set for lambda mode + Test if lambda is set for lambda mode (optimized with stateless lambda) """ # Given @@ -66,5 +66,6 @@ def test_text_config_lamda_is_set(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert "it_4->set_template([=]() -> esphome::optional {" in main_cpp + # Stateless lambda optimization: empty capture list allows function pointer conversion + assert "it_4->set_template([]() -> esphome::optional {" in main_cpp assert 'return std::string{"Hello"};' in main_cpp diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 95633ca0c6..2c9f760c8e 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -173,6 +173,61 @@ class TestLambdaExpression: "}" ) + def test_str__stateless_no_return(self): + """Test stateless lambda (empty capture) generates correctly""" + target = cg.LambdaExpression( + ('ESP_LOGD("main", "Test message");',), + (), # No parameters + "", # Empty capture (stateless) + ) + + actual = str(target) + + assert actual == ('[]() {\n ESP_LOGD("main", "Test message");\n}') + + def test_str__stateless_with_return(self): + """Test stateless lambda with return type generates correctly""" + target = cg.LambdaExpression( + ("return global_value > 0;",), + (), # No parameters + "", # Empty capture (stateless) + bool, # Return type + ) + + actual = str(target) + + assert actual == ("[]() -> bool {\n return global_value > 0;\n}") + + def test_str__stateless_with_params(self): + """Test stateless lambda with parameters generates correctly""" + target = cg.LambdaExpression( + ("return foo + bar;",), + ((int, "foo"), (float, "bar")), + "", # Empty capture (stateless) + float, + ) + + actual = str(target) + + assert actual == ( + "[](int32_t foo, float bar) -> float {\n return foo + bar;\n}" + ) + + def test_str__with_capture(self): + """Test lambda with capture generates correctly""" + target = cg.LambdaExpression( + ("return captured_var + x;",), + ((int, "x"),), + "captured_var", # Has capture (not stateless) + int, + ) + + actual = str(target) + + assert actual == ( + "[captured_var](int32_t x) -> int32_t {\n return captured_var + x;\n}" + ) + class TestLiterals: @pytest.mark.parametrize( From d7343a769d8d1f3ba2f37a910640fca0f23a5475 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 20:19:00 -0500 Subject: [PATCH 140/394] [light] Optimize LambdaLightEffect and AddressableLambdaLightEffect with function pointers --- esphome/components/light/addressable_light_effect.h | 6 +++--- esphome/components/light/base_light_effects.h | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 9840112040..0847db3770 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -57,9 +57,9 @@ class AddressableLightEffect : public LightEffect { class AddressableLambdaLightEffect : public AddressableLightEffect { public: - AddressableLambdaLightEffect(const char *name, std::function f, + AddressableLambdaLightEffect(const char *name, void (*f)(AddressableLight &, Color, bool initial_run), uint32_t update_interval) - : AddressableLightEffect(name), f_(std::move(f)), update_interval_(update_interval) {} + : AddressableLightEffect(name), f_(f), update_interval_(update_interval) {} void start() override { this->initial_run_ = true; } void apply(AddressableLight &it, const Color ¤t_color) override { const uint32_t now = millis(); @@ -72,7 +72,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect { } protected: - std::function f_; + void (*f_)(AddressableLight &, Color, bool initial_run); uint32_t update_interval_; uint32_t last_run_{0}; bool initial_run_; diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index 327c243525..515afc5c59 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -112,8 +112,8 @@ class RandomLightEffect : public LightEffect { class LambdaLightEffect : public LightEffect { public: - LambdaLightEffect(const char *name, std::function f, uint32_t update_interval) - : LightEffect(name), f_(std::move(f)), update_interval_(update_interval) {} + LambdaLightEffect(const char *name, void (*f)(bool initial_run), uint32_t update_interval) + : LightEffect(name), f_(f), update_interval_(update_interval) {} void start() override { this->initial_run_ = true; } void apply() override { @@ -130,7 +130,7 @@ class LambdaLightEffect : public LightEffect { uint32_t get_current_index() const { return this->get_index(); } protected: - std::function f_; + void (*f_)(bool initial_run); uint32_t update_interval_; uint32_t last_run_{0}; bool initial_run_; From 469dc052a5a7b85ec84d833983ab9e9155719a14 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 20:27:23 -0500 Subject: [PATCH 141/394] remov etemplate chnges --- esphome/components/template/binary_sensor/__init__.py | 8 +------- .../template/binary_sensor/template_binary_sensor.cpp | 4 ++-- .../template/binary_sensor/template_binary_sensor.h | 4 ++-- esphome/components/template/cover/template_cover.cpp | 4 ++-- esphome/components/template/cover/template_cover.h | 8 ++++---- esphome/components/template/datetime/template_date.h | 4 ++-- esphome/components/template/datetime/template_datetime.h | 4 ++-- esphome/components/template/datetime/template_time.h | 4 ++-- esphome/components/template/lock/template_lock.cpp | 2 +- esphome/components/template/lock/template_lock.h | 4 ++-- esphome/components/template/number/template_number.h | 4 ++-- esphome/components/template/select/template_select.h | 4 ++-- esphome/components/template/sensor/template_sensor.cpp | 2 +- esphome/components/template/sensor/template_sensor.h | 4 ++-- esphome/components/template/switch/template_switch.cpp | 2 +- esphome/components/template/switch/template_switch.h | 4 ++-- esphome/components/template/text/template_text.h | 4 ++-- .../template/text_sensor/template_text_sensor.cpp | 2 +- .../template/text_sensor/template_text_sensor.h | 4 ++-- esphome/components/template/valve/template_valve.cpp | 2 +- esphome/components/template/valve/template_valve.h | 4 ++-- 21 files changed, 38 insertions(+), 44 deletions(-) diff --git a/esphome/components/template/binary_sensor/__init__.py b/esphome/components/template/binary_sensor/__init__.py index 9d4208dcca..c93876380d 100644 --- a/esphome/components/template/binary_sensor/__init__.py +++ b/esphome/components/template/binary_sensor/__init__.py @@ -38,14 +38,8 @@ async def to_code(config): condition = await automation.build_condition( condition, cg.TemplateArguments(), [] ) - # Generate a stateless lambda that calls condition.check() - # capture="" is safe because condition is a global variable in generated C++ code - # and doesn't need to be captured. This allows implicit conversion to function pointer. template_ = LambdaExpression( - f"return {condition.check()};", - [], - return_type=cg.optional.template(bool), - capture="", + f"return {condition.check()};", [], return_type=cg.optional.template(bool) ) cg.add(var.set_template(template_)) diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.cpp b/esphome/components/template/binary_sensor/template_binary_sensor.cpp index 8543dff4dc..d1fb618695 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.cpp +++ b/esphome/components/template/binary_sensor/template_binary_sensor.cpp @@ -9,10 +9,10 @@ static const char *const TAG = "template.binary_sensor"; void TemplateBinarySensor::setup() { this->loop(); } void TemplateBinarySensor::loop() { - if (!this->f_.has_value()) + if (this->f_ == nullptr) return; - auto s = (*this->f_)(); + auto s = this->f_(); if (s.has_value()) { this->publish_state(*s); } diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.h b/esphome/components/template/binary_sensor/template_binary_sensor.h index 2e0b216eb4..5e5624d82e 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.h +++ b/esphome/components/template/binary_sensor/template_binary_sensor.h @@ -8,7 +8,7 @@ namespace template_ { class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor { public: - void set_template(optional (*f)()) { this->f_ = f; } + void set_template(std::function()> &&f) { this->f_ = f; } void setup() override; void loop() override; @@ -17,7 +17,7 @@ class TemplateBinarySensor : public Component, public binary_sensor::BinarySenso float get_setup_priority() const override { return setup_priority::HARDWARE; } protected: - optional (*)()> f_; + std::function()> f_{nullptr}; }; } // namespace template_ diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index bed3931e78..84c687536e 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -63,7 +63,7 @@ void TemplateCover::loop() { } void TemplateCover::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void TemplateCover::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } -void TemplateCover::set_state_lambda(optional (*f)()) { this->state_f_ = f; } +void TemplateCover::set_state_lambda(std::function()> &&f) { this->state_f_ = f; } float TemplateCover::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; } Trigger<> *TemplateCover::get_close_trigger() const { return this->close_trigger_; } @@ -124,7 +124,7 @@ CoverTraits TemplateCover::get_traits() { } Trigger *TemplateCover::get_position_trigger() const { return this->position_trigger_; } Trigger *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; } -void TemplateCover::set_tilt_lambda(optional (*tilt_f)()) { this->tilt_f_ = tilt_f; } +void TemplateCover::set_tilt_lambda(std::function()> &&tilt_f) { this->tilt_f_ = tilt_f; } void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } void TemplateCover::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index ed1ebf4e43..958c94b0a6 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -17,7 +17,7 @@ class TemplateCover : public cover::Cover, public Component { public: TemplateCover(); - void set_state_lambda(optional (*f)()); + void set_state_lambda(std::function()> &&f); Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; @@ -26,7 +26,7 @@ class TemplateCover : public cover::Cover, public Component { Trigger *get_tilt_trigger() const; void set_optimistic(bool optimistic); void set_assumed_state(bool assumed_state); - void set_tilt_lambda(optional (*tilt_f)()); + void set_tilt_lambda(std::function()> &&tilt_f); void set_has_stop(bool has_stop); void set_has_position(bool has_position); void set_has_tilt(bool has_tilt); @@ -45,8 +45,8 @@ class TemplateCover : public cover::Cover, public Component { void stop_prev_trigger_(); TemplateCoverRestoreMode restore_mode_{COVER_RESTORE}; - optional (*)()> state_f_; - optional (*)()> tilt_f_; + optional()>> state_f_; + optional()>> tilt_f_; bool assumed_state_{false}; bool optimistic_{false}; Trigger<> *open_trigger_; diff --git a/esphome/components/template/datetime/template_date.h b/esphome/components/template/datetime/template_date.h index 2a0967fc94..185c7ed49d 100644 --- a/esphome/components/template/datetime/template_date.h +++ b/esphome/components/template/datetime/template_date.h @@ -15,7 +15,7 @@ namespace template_ { class TemplateDate : public datetime::DateEntity, public PollingComponent { public: - void set_template(optional (*f)()) { this->f_ = f; } + void set_template(std::function()> &&f) { this->f_ = f; } void setup() override; void update() override; @@ -35,7 +35,7 @@ class TemplateDate : public datetime::DateEntity, public PollingComponent { ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional (*)()> f_; + optional()>> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/datetime/template_datetime.h b/esphome/components/template/datetime/template_datetime.h index d917015b67..ef80ded89a 100644 --- a/esphome/components/template/datetime/template_datetime.h +++ b/esphome/components/template/datetime/template_datetime.h @@ -15,7 +15,7 @@ namespace template_ { class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent { public: - void set_template(optional (*f)()) { this->f_ = f; } + void set_template(std::function()> &&f) { this->f_ = f; } void setup() override; void update() override; @@ -35,7 +35,7 @@ class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponen ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional (*)()> f_; + optional()>> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/datetime/template_time.h b/esphome/components/template/datetime/template_time.h index 2f05ba0737..4a7c0098ec 100644 --- a/esphome/components/template/datetime/template_time.h +++ b/esphome/components/template/datetime/template_time.h @@ -15,7 +15,7 @@ namespace template_ { class TemplateTime : public datetime::TimeEntity, public PollingComponent { public: - void set_template(optional (*f)()) { this->f_ = f; } + void set_template(std::function()> &&f) { this->f_ = f; } void setup() override; void update() override; @@ -35,7 +35,7 @@ class TemplateTime : public datetime::TimeEntity, public PollingComponent { ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional (*)()> f_; + optional()>> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/lock/template_lock.cpp b/esphome/components/template/lock/template_lock.cpp index c2e227c26d..87ba1046eb 100644 --- a/esphome/components/template/lock/template_lock.cpp +++ b/esphome/components/template/lock/template_lock.cpp @@ -45,7 +45,7 @@ void TemplateLock::open_latch() { this->open_trigger_->trigger(); } void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } -void TemplateLock::set_state_lambda(optional (*f)()) { this->f_ = f; } +void TemplateLock::set_state_lambda(std::function()> &&f) { this->f_ = f; } float TemplateLock::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateLock::get_lock_trigger() const { return this->lock_trigger_; } Trigger<> *TemplateLock::get_unlock_trigger() const { return this->unlock_trigger_; } diff --git a/esphome/components/template/lock/template_lock.h b/esphome/components/template/lock/template_lock.h index 428744a66f..4f798eca81 100644 --- a/esphome/components/template/lock/template_lock.h +++ b/esphome/components/template/lock/template_lock.h @@ -13,7 +13,7 @@ class TemplateLock : public lock::Lock, public Component { void dump_config() override; - void set_state_lambda(optional (*f)()); + void set_state_lambda(std::function()> &&f); Trigger<> *get_lock_trigger() const; Trigger<> *get_unlock_trigger() const; Trigger<> *get_open_trigger() const; @@ -26,7 +26,7 @@ class TemplateLock : public lock::Lock, public Component { void control(const lock::LockCall &call) override; void open_latch() override; - optional (*)()> f_; + optional()>> f_; bool optimistic_{false}; Trigger<> *lock_trigger_; Trigger<> *unlock_trigger_; diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h index e77b181d25..9a82e44339 100644 --- a/esphome/components/template/number/template_number.h +++ b/esphome/components/template/number/template_number.h @@ -10,7 +10,7 @@ namespace template_ { class TemplateNumber : public number::Number, public PollingComponent { public: - void set_template(optional (*f)()) { this->f_ = f; } + void set_template(std::function()> &&f) { this->f_ = f; } void setup() override; void update() override; @@ -28,7 +28,7 @@ class TemplateNumber : public number::Number, public PollingComponent { float initial_value_{NAN}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional (*)()> f_; + optional()>> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index c1b348b26a..2f00765c3d 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -10,7 +10,7 @@ namespace template_ { class TemplateSelect : public select::Select, public PollingComponent { public: - void set_template(optional (*f)()) { this->f_ = f; } + void set_template(std::function()> &&f) { this->f_ = f; } void setup() override; void update() override; @@ -28,7 +28,7 @@ class TemplateSelect : public select::Select, public PollingComponent { std::string initial_option_; bool restore_value_ = false; Trigger *set_trigger_ = new Trigger(); - optional (*)()> f_; + optional()>> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/sensor/template_sensor.cpp b/esphome/components/template/sensor/template_sensor.cpp index 65f2417670..f2d0e7363e 100644 --- a/esphome/components/template/sensor/template_sensor.cpp +++ b/esphome/components/template/sensor/template_sensor.cpp @@ -17,7 +17,7 @@ void TemplateSensor::update() { } } float TemplateSensor::get_setup_priority() const { return setup_priority::HARDWARE; } -void TemplateSensor::set_template(optional (*f)()) { this->f_ = f; } +void TemplateSensor::set_template(std::function()> &&f) { this->f_ = f; } void TemplateSensor::dump_config() { LOG_SENSOR("", "Template Sensor", this); LOG_UPDATE_INTERVAL(this); diff --git a/esphome/components/template/sensor/template_sensor.h b/esphome/components/template/sensor/template_sensor.h index 369313d607..2630cb0b14 100644 --- a/esphome/components/template/sensor/template_sensor.h +++ b/esphome/components/template/sensor/template_sensor.h @@ -8,7 +8,7 @@ namespace template_ { class TemplateSensor : public sensor::Sensor, public PollingComponent { public: - void set_template(optional (*f)()); + void set_template(std::function()> &&f); void update() override; @@ -17,7 +17,7 @@ class TemplateSensor : public sensor::Sensor, public PollingComponent { float get_setup_priority() const override; protected: - optional (*)()> f_; + optional()>> f_; }; } // namespace template_ diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index 5aaf514b2a..fa236f6364 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -35,7 +35,7 @@ void TemplateSwitch::write_state(bool state) { } void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } bool TemplateSwitch::assumed_state() { return this->assumed_state_; } -void TemplateSwitch::set_state_lambda(optional (*f)()) { this->f_ = f; } +void TemplateSwitch::set_state_lambda(std::function()> &&f) { this->f_ = f; } float TemplateSwitch::get_setup_priority() const { return setup_priority::HARDWARE - 2.0f; } Trigger<> *TemplateSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } Trigger<> *TemplateSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index 0fba66b9bd..bfe9ac25d6 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -14,7 +14,7 @@ class TemplateSwitch : public switch_::Switch, public Component { void setup() override; void dump_config() override; - void set_state_lambda(optional (*f)()); + void set_state_lambda(std::function()> &&f); Trigger<> *get_turn_on_trigger() const; Trigger<> *get_turn_off_trigger() const; void set_optimistic(bool optimistic); @@ -28,7 +28,7 @@ class TemplateSwitch : public switch_::Switch, public Component { void write_state(bool state) override; - optional (*)()> f_; + optional()>> f_; bool optimistic_{false}; bool assumed_state_{false}; Trigger<> *turn_on_trigger_; diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index 6c17d2016a..bcfc54a2ba 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -61,7 +61,7 @@ template class TextSaver : public TemplateTextSaverBase { class TemplateText : public text::Text, public PollingComponent { public: - void set_template(optional (*f)()) { this->f_ = f; } + void set_template(std::function()> &&f) { this->f_ = f; } void setup() override; void update() override; @@ -78,7 +78,7 @@ class TemplateText : public text::Text, public PollingComponent { bool optimistic_ = false; std::string initial_value_; Trigger *set_trigger_ = new Trigger(); - optional (*)()> f_{nullptr}; + optional()>> f_{nullptr}; TemplateTextSaverBase *pref_ = nullptr; }; diff --git a/esphome/components/template/text_sensor/template_text_sensor.cpp b/esphome/components/template/text_sensor/template_text_sensor.cpp index 2b0297d62f..885ad47bbf 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.cpp +++ b/esphome/components/template/text_sensor/template_text_sensor.cpp @@ -16,7 +16,7 @@ void TemplateTextSensor::update() { } } float TemplateTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } -void TemplateTextSensor::set_template(optional (*f)()) { this->f_ = f; } +void TemplateTextSensor::set_template(std::function()> &&f) { this->f_ = f; } void TemplateTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Template Sensor", this); } } // namespace template_ diff --git a/esphome/components/template/text_sensor/template_text_sensor.h b/esphome/components/template/text_sensor/template_text_sensor.h index 48e40c2493..07a2bd96fc 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.h +++ b/esphome/components/template/text_sensor/template_text_sensor.h @@ -9,7 +9,7 @@ namespace template_ { class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent { public: - void set_template(optional (*f)()); + void set_template(std::function()> &&f); void update() override; @@ -18,7 +18,7 @@ class TemplateTextSensor : public text_sensor::TextSensor, public PollingCompone void dump_config() override; protected: - optional (*)()> f_{}; + optional()>> f_{}; }; } // namespace template_ diff --git a/esphome/components/template/valve/template_valve.cpp b/esphome/components/template/valve/template_valve.cpp index b27cc00968..5fa14a2de7 100644 --- a/esphome/components/template/valve/template_valve.cpp +++ b/esphome/components/template/valve/template_valve.cpp @@ -55,7 +55,7 @@ void TemplateValve::loop() { void TemplateValve::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void TemplateValve::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } -void TemplateValve::set_state_lambda(optional (*f)()) { this->state_f_ = f; } +void TemplateValve::set_state_lambda(std::function()> &&f) { this->state_f_ = f; } float TemplateValve::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateValve::get_open_trigger() const { return this->open_trigger_; } diff --git a/esphome/components/template/valve/template_valve.h b/esphome/components/template/valve/template_valve.h index 92c32f3487..5e3fb6aff3 100644 --- a/esphome/components/template/valve/template_valve.h +++ b/esphome/components/template/valve/template_valve.h @@ -17,7 +17,7 @@ class TemplateValve : public valve::Valve, public Component { public: TemplateValve(); - void set_state_lambda(optional (*f)()); + void set_state_lambda(std::function()> &&f); Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; @@ -42,7 +42,7 @@ class TemplateValve : public valve::Valve, public Component { void stop_prev_trigger_(); TemplateValveRestoreMode restore_mode_{VALVE_NO_RESTORE}; - optional (*)()> state_f_; + optional()>> state_f_; bool assumed_state_{false}; bool optimistic_{false}; Trigger<> *open_trigger_; From c0f9a0ed839ff2d150f4e1b83eb7ba43c0837ee2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 20:27:41 -0500 Subject: [PATCH 142/394] remov etemplate chnges --- tests/component_tests/text/test_text.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 1cc31a288b..75f1c4b88b 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -58,7 +58,7 @@ def test_text_config_value_mode_set(generate_main): def test_text_config_lamda_is_set(generate_main): """ - Test if lambda is set for lambda mode (optimized with stateless lambda) + Test if lambda is set for lambda mode """ # Given @@ -66,6 +66,5 @@ def test_text_config_lamda_is_set(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - # Stateless lambda optimization: empty capture list allows function pointer conversion - assert "it_4->set_template([]() -> esphome::optional {" in main_cpp + assert "it_4->set_template([=]() -> esphome::optional {" in main_cpp assert 'return std::string{"Hello"};' in main_cpp From 8789e8637c06f052935ad095d761da53850d11ef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 20:31:08 -0500 Subject: [PATCH 143/394] merge --- tests/component_tests/text/test_text.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 75f1c4b88b..1cc31a288b 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -58,7 +58,7 @@ def test_text_config_value_mode_set(generate_main): def test_text_config_lamda_is_set(generate_main): """ - Test if lambda is set for lambda mode + Test if lambda is set for lambda mode (optimized with stateless lambda) """ # Given @@ -66,5 +66,6 @@ def test_text_config_lamda_is_set(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert "it_4->set_template([=]() -> esphome::optional {" in main_cpp + # Stateless lambda optimization: empty capture list allows function pointer conversion + assert "it_4->set_template([]() -> esphome::optional {" in main_cpp assert 'return std::string{"Hello"};' in main_cpp From f676759e04d1413a3fb16a92d4ecb45aa8cad6dd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Oct 2025 11:22:36 -0500 Subject: [PATCH 144/394] preen --- ard_esp32_opentherm_tests_pr.md | 86 --------------------------------- 1 file changed, 86 deletions(-) delete mode 100644 ard_esp32_opentherm_tests_pr.md diff --git a/ard_esp32_opentherm_tests_pr.md b/ard_esp32_opentherm_tests_pr.md deleted file mode 100644 index b543109d56..0000000000 --- a/ard_esp32_opentherm_tests_pr.md +++ /dev/null @@ -1,86 +0,0 @@ -# What does this implement/fix? - -Removes redundant ESP32 Arduino test files for the `opentherm` component and cleans up redundant preprocessor conditionals. The ESP-IDF tests provide complete coverage since the opentherm component has no framework-specific implementation differences for ESP32. - -Also fixes incorrect preprocessor conditionals - changes `#if defined(ESP32) || defined(USE_ESP_IDF)` to `#ifdef USE_ESP32`. The macro `ESP32` is only defined for the original ESP32 variant, while `USE_ESP32` covers all ESP32 variants (C3, S2, S3, etc.). The `|| defined(USE_ESP_IDF)` was unnecessary since ESP-IDF can only run on ESP32 platforms. - -## Background - -As part of the ongoing effort to reduce Arduino-specific test redundancy (esphome/backlog#66), this PR removes ESP32 Arduino tests that duplicate IDF test coverage. - -**Analysis of opentherm component:** -- Previously used `#if defined(ESP32) || defined(USE_ESP_IDF)` to check for ESP32 **platform** -- This was incorrect: `ESP32` is only defined for the original ESP32 variant, not C3/S2/S3 -- Changed to `#ifdef USE_ESP32` which covers all ESP32 variants -- The `|| defined(USE_ESP_IDF)` part was unnecessary since ESP-IDF can only run on ESP32 platforms -- ESP32 timer APIs (`timer_init`, `timer_set_counter_value`, `timer_isr_callback_add`) are ESP-IDF APIs -- These timer APIs work identically in both Arduino and ESP-IDF frameworks since Arduino is now built on ESP-IDF -- Only ESP8266 has framework-specific code (using Arduino's `timer1_*` functions) -- ESP32 implementation is identical across frameworks - -## Changes - -### Code Cleanup - -**OpenTherm component:** -- Fixed incorrect `#if defined(ESP32) || defined(USE_ESP_IDF)` to `#ifdef USE_ESP32` in: - - `esphome/components/opentherm/opentherm.h` (3 locations) - - `esphome/components/opentherm/opentherm.cpp` (4 locations) -- `ESP32` is only defined for the original ESP32 variant, not C3/S2/S3 -- `USE_ESP32` correctly covers all ESP32 variants -- The `|| defined(USE_ESP_IDF)` part was unnecessary since ESP-IDF can only be defined on ESP32 platforms - -### Test Files Removed - -- `tests/components/opentherm/test.esp32-ard.yaml` -- `tests/components/opentherm/test.esp32-c3-ard.yaml` - -### Test Coverage Maintained - -ESP-IDF test files remain and cover both frameworks: -- `tests/components/opentherm/test.esp32-idf.yaml` -- `tests/components/opentherm/test.esp32-c3-idf.yaml` - -### Platform-Specific Tests Retained - -Arduino tests remain for ESP8266 (uses Arduino-specific `timer1_*` functions): -- `tests/components/opentherm/test.esp8266-ard.yaml` - -## Benefits - -- **Reduces CI test time** - 2 fewer redundant test configurations -- **Simplifies code** - Removes redundant preprocessor conditionals -- **Maintains coverage** - IDF tests cover both frameworks for ESP32 - -## Types of changes - -- [x] Code quality improvements to existing code or addition of tests - -**Related issue or feature (if applicable):** - -- Part of esphome/backlog#66 - Remove redundant ESP32 Arduino tests - -**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):** - -N/A - No user-facing changes - -## Test Environment - -- [x] ESP32 -- [x] ESP32 IDF -- [ ] ESP8266 -- [ ] RP2040 -- [ ] BK72xx -- [ ] RTL87xx -- [ ] nRF52840 - -## Example entry for `config.yaml`: - -N/A - No configuration changes - -## Checklist: - - [x] The code change is tested and works locally. - - [ ] Tests have been added to verify that the new code works (under `tests/` folder). - -If user exposed functionality or configuration variables are added/changed: - - [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs). From 8704c6d231da666b8264c8028c9b966b4b0b93ec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Oct 2025 11:22:47 -0500 Subject: [PATCH 145/394] preen --- esphome/analyze_memory.py | 1620 ------------------------------------- 1 file changed, 1620 deletions(-) delete mode 100644 esphome/analyze_memory.py diff --git a/esphome/analyze_memory.py b/esphome/analyze_memory.py deleted file mode 100644 index 8fac423faa..0000000000 --- a/esphome/analyze_memory.py +++ /dev/null @@ -1,1620 +0,0 @@ -"""Memory usage analyzer for ESPHome compiled binaries.""" - -from collections import defaultdict -import json -import logging -from pathlib import Path -import re -import subprocess - -_LOGGER = logging.getLogger(__name__) - -# Pattern to extract ESPHome component namespaces dynamically -ESPHOME_COMPONENT_PATTERN = re.compile(r"esphome::([a-zA-Z0-9_]+)::") - -# Component identification rules -# Symbol patterns: patterns found in raw symbol names -SYMBOL_PATTERNS = { - "freertos": [ - "vTask", - "xTask", - "xQueue", - "pvPort", - "vPort", - "uxTask", - "pcTask", - "prvTimerTask", - "prvAddNewTaskToReadyList", - "pxReadyTasksLists", - "prvAddCurrentTaskToDelayedList", - "xEventGroupWaitBits", - "xRingbufferSendFromISR", - "prvSendItemDoneNoSplit", - "prvReceiveGeneric", - "prvSendAcquireGeneric", - "prvCopyItemAllowSplit", - "xEventGroup", - "xRingbuffer", - "prvSend", - "prvReceive", - "prvCopy", - "xPort", - "ulTaskGenericNotifyTake", - "prvIdleTask", - "prvInitialiseNewTask", - "prvIsYieldRequiredSMP", - "prvGetItemByteBuf", - "prvInitializeNewRingbuffer", - "prvAcquireItemNoSplit", - "prvNotifyQueueSetContainer", - "ucStaticTimerQueueStorage", - "eTaskGetState", - "main_task", - "do_system_init_fn", - "xSemaphoreCreateGenericWithCaps", - "vListInsert", - "uxListRemove", - "vRingbufferReturnItem", - "vRingbufferReturnItemFromISR", - "prvCheckItemFitsByteBuffer", - "prvGetCurMaxSizeAllowSplit", - "tick_hook", - "sys_sem_new", - "sys_arch_mbox_fetch", - "sys_arch_sem_wait", - "prvDeleteTCB", - "vQueueDeleteWithCaps", - "vRingbufferDeleteWithCaps", - "vSemaphoreDeleteWithCaps", - "prvCheckItemAvail", - "prvCheckTaskCanBeScheduledSMP", - "prvGetCurMaxSizeNoSplit", - "prvResetNextTaskUnblockTime", - "prvReturnItemByteBuf", - "vApplicationStackOverflowHook", - "vApplicationGetIdleTaskMemory", - "sys_init", - "sys_mbox_new", - "sys_arch_mbox_tryfetch", - ], - "xtensa": ["xt_", "_xt_", "xPortEnterCriticalTimeout"], - "heap": ["heap_", "multi_heap"], - "spi_flash": ["spi_flash"], - "rtc": ["rtc_", "rtcio_ll_"], - "gpio_driver": ["gpio_", "pins"], - "uart_driver": ["uart", "_uart", "UART"], - "timer": ["timer_", "esp_timer"], - "peripherals": ["periph_", "periman"], - "network_stack": [ - "vj_compress", - "raw_sendto", - "raw_input", - "etharp_", - "icmp_input", - "socket_ipv6", - "ip_napt", - "socket_ipv4_multicast", - "socket_ipv6_multicast", - "netconn_", - "recv_raw", - "accept_function", - "netconn_recv_data", - "netconn_accept", - "netconn_write_vectors_partly", - "netconn_drain", - "raw_connect", - "raw_bind", - "icmp_send_response", - "sockets", - "icmp_dest_unreach", - "inet_chksum_pseudo", - "alloc_socket", - "done_socket", - "set_global_fd_sets", - "inet_chksum_pbuf", - "tryget_socket_unconn_locked", - "tryget_socket_unconn", - "cs_create_ctrl_sock", - "netbuf_alloc", - ], - "ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"], - "wifi_stack": [ - "ieee80211", - "hostap", - "sta_", - "ap_", - "scan_", - "wifi_", - "wpa_", - "wps_", - "esp_wifi", - "cnx_", - "wpa3_", - "sae_", - "wDev_", - "ic_", - "mac_", - "esf_buf", - "gWpaSm", - "sm_WPA", - "eapol_", - "owe_", - "wifiLowLevelInit", - "s_do_mapping", - "gScanStruct", - "ppSearchTxframe", - "ppMapWaitTxq", - "ppFillAMPDUBar", - "ppCheckTxConnTrafficIdle", - "ppCalTkipMic", - ], - "bluetooth": ["bt_", "ble_", "l2c_", "gatt_", "gap_", "hci_", "BT_init"], - "wifi_bt_coex": ["coex"], - "bluetooth_rom": ["r_ble", "r_lld", "r_llc", "r_llm"], - "bluedroid_bt": [ - "bluedroid", - "btc_", - "bta_", - "btm_", - "btu_", - "BTM_", - "GATT", - "L2CA_", - "smp_", - "gatts_", - "attp_", - "l2cu_", - "l2cb", - "smp_cb", - "BTA_GATTC_", - "SMP_", - "BTU_", - "BTA_Dm", - "GAP_Ble", - "BT_tx_if", - "host_recv_pkt_cb", - "saved_local_oob_data", - "string_to_bdaddr", - "string_is_bdaddr", - "CalConnectParamTimeout", - "transmit_fragment", - "transmit_data", - "event_command_ready", - "read_command_complete_header", - "parse_read_local_extended_features_response", - "parse_read_local_version_info_response", - "should_request_high", - "btdm_wakeup_request", - "BTA_SetAttributeValue", - "BTA_EnableBluetooth", - "transmit_command_futured", - "transmit_command", - "get_waiting_command", - "make_command", - "transmit_downward", - "host_recv_adv_packet", - "copy_extra_byte_in_db", - "parse_read_local_supported_commands_response", - ], - "crypto_math": [ - "ecp_", - "bignum_", - "mpi_", - "sswu", - "modp", - "dragonfly_", - "gcm_mult", - "__multiply", - "quorem", - "__mdiff", - "__lshift", - "__mprec_tens", - "ECC_", - "multiprecision_", - "mix_sub_columns", - "sbox", - "gfm2_sbox", - "gfm3_sbox", - "curve_p256", - "curve", - "p_256_init_curve", - "shift_sub_rows", - "rshift", - ], - "hw_crypto": ["esp_aes", "esp_sha", "esp_rsa", "esp_bignum", "esp_mpi"], - "libc": [ - "printf", - "scanf", - "malloc", - "free", - "memcpy", - "memset", - "strcpy", - "strlen", - "_dtoa", - "_fopen", - "__sfvwrite_r", - "qsort", - "__sf", - "__sflush_r", - "__srefill_r", - "_impure_data", - "_reclaim_reent", - "_open_r", - "strncpy", - "_strtod_l", - "__gethex", - "__hexnan", - "_setenv_r", - "_tzset_unlocked_r", - "__tzcalc_limits", - "select", - "scalbnf", - "strtof", - "strtof_l", - "__d2b", - "__b2d", - "__s2b", - "_Balloc", - "__multadd", - "__lo0bits", - "__atexit0", - "__smakebuf_r", - "__swhatbuf_r", - "_sungetc_r", - "_close_r", - "_link_r", - "_unsetenv_r", - "_rename_r", - "__month_lengths", - "tzinfo", - "__ratio", - "__hi0bits", - "__ulp", - "__any_on", - "__copybits", - "L_shift", - "_fcntl_r", - "_lseek_r", - "_read_r", - "_write_r", - "_unlink_r", - "_fstat_r", - "access", - "fsync", - "tcsetattr", - "tcgetattr", - "tcflush", - "tcdrain", - "__ssrefill_r", - "_stat_r", - "__hexdig_fun", - "__mcmp", - "_fwalk_sglue", - "__fpclassifyf", - "_setlocale_r", - "_mbrtowc_r", - "fcntl", - "__match", - "_lock_close", - "__c$", - "__func__$", - "__FUNCTION__$", - "DAYS_IN_MONTH", - "_DAYS_BEFORE_MONTH", - "CSWTCH$", - "dst$", - "sulp", - ], - "string_ops": ["strcmp", "strncmp", "strchr", "strstr", "strtok", "strdup"], - "memory_alloc": ["malloc", "calloc", "realloc", "free", "_sbrk"], - "file_io": [ - "fread", - "fwrite", - "fopen", - "fclose", - "fseek", - "ftell", - "fflush", - "s_fd_table", - ], - "string_formatting": [ - "snprintf", - "vsnprintf", - "sprintf", - "vsprintf", - "sscanf", - "vsscanf", - ], - "cpp_anonymous": ["_GLOBAL__N_", "n$"], - "cpp_runtime": ["__cxx", "_ZN", "_ZL", "_ZSt", "__gxx_personality", "_Z16"], - "exception_handling": ["__cxa_", "_Unwind_", "__gcc_personality", "uw_frame_state"], - "static_init": ["_GLOBAL__sub_I_"], - "mdns_lib": ["mdns"], - "phy_radio": [ - "phy_", - "rf_", - "chip_", - "register_chipv7", - "pbus_", - "bb_", - "fe_", - "rfcal_", - "ram_rfcal", - "tx_pwctrl", - "rx_chan", - "set_rx_gain", - "set_chan", - "agc_reg", - "ram_txiq", - "ram_txdc", - "ram_gen_rx_gain", - "rx_11b_opt", - "set_rx_sense", - "set_rx_gain_cal", - "set_chan_dig_gain", - "tx_pwctrl_init_cal", - "rfcal_txiq", - "set_tx_gain_table", - "correct_rfpll_offset", - "pll_correct_dcap", - "txiq_cal_init", - "pwdet_sar", - "pwdet_sar2_init", - "ram_iq_est_enable", - "ram_rfpll_set_freq", - "ant_wifirx_cfg", - "ant_btrx_cfg", - "force_txrxoff", - "force_txrx_off", - "tx_paon_set", - "opt_11b_resart", - "rfpll_1p2_opt", - "ram_dc_iq_est", - "ram_start_tx_tone", - "ram_en_pwdet", - "ram_cbw2040_cfg", - "rxdc_est_min", - "i2cmst_reg_init", - "temprature_sens_read", - "ram_restart_cal", - "ram_write_gain_mem", - "ram_wait_rfpll_cal_end", - "txcal_debuge_mode", - "ant_wifitx_cfg", - "reg_init_begin", - ], - "wifi_phy_pp": ["pp_", "ppT", "ppR", "ppP", "ppInstall", "ppCalTxAMPDULength"], - "wifi_lmac": ["lmac"], - "wifi_device": ["wdev", "wDev_"], - "power_mgmt": [ - "pm_", - "sleep", - "rtc_sleep", - "light_sleep", - "deep_sleep", - "power_down", - "g_pm", - ], - "memory_mgmt": [ - "mem_", - "memory_", - "tlsf_", - "memp_", - "pbuf_", - "pbuf_alloc", - "pbuf_copy_partial_pbuf", - ], - "hal_layer": ["hal_"], - "clock_mgmt": [ - "clk_", - "clock_", - "rtc_clk", - "apb_", - "cpu_freq", - "setCpuFrequencyMhz", - ], - "cache_mgmt": ["cache"], - "flash_ops": ["flash", "image_load"], - "interrupt_handlers": [ - "isr", - "interrupt", - "intr_", - "exc_", - "exception", - "port_IntStack", - ], - "wrapper_functions": ["_wrapper"], - "error_handling": ["panic", "abort", "assert", "error_", "fault"], - "authentication": ["auth"], - "ppp_protocol": ["ppp", "ipcp_", "lcp_", "chap_", "LcpEchoCheck"], - "dhcp": ["dhcp", "handle_dhcp"], - "ethernet_phy": [ - "emac_", - "eth_phy_", - "phy_tlk110", - "phy_lan87", - "phy_ip101", - "phy_rtl", - "phy_dp83", - "phy_ksz", - "lan87xx_", - "rtl8201_", - "ip101_", - "ksz80xx_", - "jl1101_", - "dp83848_", - "eth_on_state_changed", - ], - "threading": ["pthread_", "thread_", "_task_"], - "pthread": ["pthread"], - "synchronization": ["mutex", "semaphore", "spinlock", "portMUX"], - "math_lib": [ - "sin", - "cos", - "tan", - "sqrt", - "pow", - "exp", - "log", - "atan", - "asin", - "acos", - "floor", - "ceil", - "fabs", - "round", - ], - "random": ["rand", "random", "rng_", "prng"], - "time_lib": [ - "time", - "clock", - "gettimeofday", - "settimeofday", - "localtime", - "gmtime", - "mktime", - "strftime", - ], - "console_io": ["console_", "uart_tx", "uart_rx", "puts", "putchar", "getchar"], - "rom_functions": ["r_", "rom_"], - "compiler_runtime": [ - "__divdi3", - "__udivdi3", - "__moddi3", - "__muldi3", - "__ashldi3", - "__ashrdi3", - "__lshrdi3", - "__cmpdi2", - "__fixdfdi", - "__floatdidf", - ], - "libgcc": ["libgcc", "_divdi3", "_udivdi3"], - "boot_startup": ["boot", "start_cpu", "call_start", "startup", "bootloader"], - "bootloader": ["bootloader_", "esp_bootloader"], - "app_framework": ["app_", "initArduino", "setup", "loop", "Update"], - "weak_symbols": ["__weak_"], - "compiler_builtins": ["__builtin_"], - "vfs": ["vfs_", "VFS"], - "esp32_sdk": ["esp32_", "esp32c", "esp32s"], - "usb": ["usb_", "USB", "cdc_", "CDC"], - "i2c_driver": ["i2c_", "I2C"], - "i2s_driver": ["i2s_", "I2S"], - "spi_driver": ["spi_", "SPI"], - "adc_driver": ["adc_", "ADC"], - "dac_driver": ["dac_", "DAC"], - "touch_driver": ["touch_", "TOUCH"], - "pwm_driver": ["pwm_", "PWM", "ledc_", "LEDC"], - "rmt_driver": ["rmt_", "RMT"], - "pcnt_driver": ["pcnt_", "PCNT"], - "can_driver": ["can_", "CAN", "twai_", "TWAI"], - "sdmmc_driver": ["sdmmc_", "SDMMC", "sdcard", "sd_card"], - "temp_sensor": ["temp_sensor", "tsens_"], - "watchdog": ["wdt_", "WDT", "watchdog"], - "brownout": ["brownout", "bod_"], - "ulp": ["ulp_", "ULP"], - "psram": ["psram", "PSRAM", "spiram", "SPIRAM"], - "efuse": ["efuse", "EFUSE"], - "partition": ["partition", "esp_partition"], - "esp_event": ["esp_event", "event_loop", "event_callback"], - "esp_console": ["esp_console", "console_"], - "chip_specific": ["chip_", "esp_chip"], - "esp_system_utils": ["esp_system", "esp_hw", "esp_clk", "esp_sleep"], - "ipc": ["esp_ipc", "ipc_"], - "wifi_config": [ - "g_cnxMgr", - "gChmCxt", - "g_ic", - "TxRxCxt", - "s_dp", - "s_ni", - "s_reg_dump", - "packet$", - "d_mult_table", - "K", - "fcstab", - ], - "smartconfig": ["sc_ack_send"], - "rc_calibration": ["rc_cal", "rcUpdate"], - "noise_floor": ["noise_check"], - "rf_calibration": [ - "set_rx_sense", - "set_rx_gain_cal", - "set_chan_dig_gain", - "tx_pwctrl_init_cal", - "rfcal_txiq", - "set_tx_gain_table", - "correct_rfpll_offset", - "pll_correct_dcap", - "txiq_cal_init", - "pwdet_sar", - "rx_11b_opt", - ], - "wifi_crypto": [ - "pk_use_ecparams", - "process_segments", - "ccmp_", - "rc4_", - "aria_", - "mgf_mask", - "dh_group", - "ccmp_aad_nonce", - "ccmp_encrypt", - "rc4_skip", - "aria_sb1", - "aria_sb2", - "aria_is1", - "aria_is2", - "aria_sl", - "aria_a", - ], - "radio_control": ["fsm_input", "fsm_sconfreq"], - "pbuf": [ - "pbuf_", - ], - "event_group": ["xEventGroup"], - "ringbuffer": ["xRingbuffer", "prvSend", "prvReceive", "prvCopy"], - "provisioning": ["prov_", "prov_stop_and_notify"], - "scan": ["gScanStruct"], - "port": ["xPort"], - "elf_loader": [ - "elf_add", - "elf_add_note", - "elf_add_segment", - "process_image", - "read_encoded", - "read_encoded_value", - "read_encoded_value_with_base", - "process_image_header", - ], - "socket_api": [ - "sockets", - "netconn_", - "accept_function", - "recv_raw", - "socket_ipv4_multicast", - "socket_ipv6_multicast", - ], - "igmp": ["igmp_", "igmp_send", "igmp_input"], - "icmp6": ["icmp6_"], - "arp": ["arp_table"], - "ampdu": [ - "ampdu_", - "rcAmpdu", - "trc_onAmpduOp", - "rcAmpduLowerRate", - "ampdu_dispatch_upto", - ], - "ieee802_11": ["ieee802_11_", "ieee802_11_parse_elems"], - "rate_control": ["rssi_margin", "rcGetSched", "get_rate_fcc_index"], - "nan": ["nan_dp_", "nan_dp_post_tx", "nan_dp_delete_peer"], - "channel_mgmt": ["chm_init", "chm_set_current_channel"], - "trace": ["trc_init", "trc_onAmpduOp"], - "country_code": ["country_info", "country_info_24ghz"], - "multicore": ["do_multicore_settings"], - "Update_lib": ["Update"], - "stdio": [ - "__sf", - "__sflush_r", - "__srefill_r", - "_impure_data", - "_reclaim_reent", - "_open_r", - ], - "strncpy_ops": ["strncpy"], - "math_internal": ["__mdiff", "__lshift", "__mprec_tens", "quorem"], - "character_class": ["__chclass"], - "camellia": ["camellia_", "camellia_feistel"], - "crypto_tables": ["FSb", "FSb2", "FSb3", "FSb4"], - "event_buffer": ["g_eb_list_desc", "eb_space"], - "base_node": ["base_node_", "base_node_add_handler"], - "file_descriptor": ["s_fd_table"], - "tx_delay": ["tx_delay_cfg"], - "deinit": ["deinit_functions"], - "lcp_echo": ["LcpEchoCheck"], - "raw_api": ["raw_bind", "raw_connect"], - "checksum": ["process_checksum"], - "entry_management": ["add_entry"], - "esp_ota": ["esp_ota", "ota_", "read_otadata"], - "http_server": [ - "httpd_", - "parse_url_char", - "cb_headers_complete", - "delete_entry", - "validate_structure", - "config_save", - "config_new", - "verify_url", - "cb_url", - ], - "misc_system": [ - "alarm_cbs", - "start_up", - "tokens", - "unhex", - "osi_funcs_ro", - "enum_function", - "fragment_and_dispatch", - "alarm_set", - "osi_alarm_new", - "config_set_string", - "config_update_newest_section", - "config_remove_key", - "method_strings", - "interop_match", - "interop_database", - "__state_table", - "__action_table", - "s_stub_table", - "s_context", - "s_mmu_ctx", - "s_get_bus_mask", - "hli_queue_put", - "list_remove", - "list_delete", - "lock_acquire_generic", - "is_vect_desc_usable", - "io_mode_str", - "__c$20233", - "interface", - "read_id_core", - "subscribe_idle", - "unsubscribe_idle", - "s_clkout_handle", - "lock_release_generic", - "config_set_int", - "config_get_int", - "config_get_string", - "config_has_key", - "config_remove_section", - "osi_alarm_init", - "osi_alarm_deinit", - "fixed_queue_enqueue", - "fixed_queue_dequeue", - "fixed_queue_new", - "fixed_pkt_queue_enqueue", - "fixed_pkt_queue_new", - "list_append", - "list_prepend", - "list_insert_after", - "list_contains", - "list_get_node", - "hash_function_blob", - "cb_no_body", - "cb_on_body", - "profile_tab", - "get_arg", - "trim", - "buf$", - "process_appended_hash_and_sig$constprop$0", - "uuidType", - "allocate_svc_db_buf", - "_hostname_is_ours", - "s_hli_handlers", - "tick_cb", - "idle_cb", - "input", - "entry_find", - "section_find", - "find_bucket_entry_", - "config_has_section", - "hli_queue_create", - "hli_queue_get", - "hli_c_handler", - "future_ready", - "future_await", - "future_new", - "pkt_queue_enqueue", - "pkt_queue_dequeue", - "pkt_queue_cleanup", - "pkt_queue_create", - "pkt_queue_destroy", - "fixed_pkt_queue_dequeue", - "osi_alarm_cancel", - "osi_alarm_is_active", - "osi_sem_take", - "osi_event_create", - "osi_event_bind", - "alarm_cb_handler", - "list_foreach", - "list_back", - "list_front", - "list_clear", - "fixed_queue_try_peek_first", - "translate_path", - "get_idx", - "find_key", - "init", - "end", - "start", - "set_read_value", - "copy_address_list", - "copy_and_key", - "sdk_cfg_opts", - "leftshift_onebit", - "config_section_end", - "config_section_begin", - "find_entry_and_check_all_reset", - "image_validate", - "xPendingReadyList", - "vListInitialise", - "lock_init_generic", - "ant_bttx_cfg", - "ant_dft_cfg", - "cs_send_to_ctrl_sock", - "config_llc_util_funcs_reset", - "make_set_adv_report_flow_control", - "make_set_event_mask", - "raw_new", - "raw_remove", - "BTE_InitStack", - "parse_read_local_supported_features_response", - "__math_invalidf", - "tinytens", - "__mprec_tinytens", - "__mprec_bigtens", - "vRingbufferDelete", - "vRingbufferDeleteWithCaps", - "vRingbufferReturnItem", - "vRingbufferReturnItemFromISR", - "get_acl_data_size_ble", - "get_features_ble", - "get_features_classic", - "get_acl_packet_size_ble", - "get_acl_packet_size_classic", - "supports_extended_inquiry_response", - "supports_rssi_with_inquiry_results", - "supports_interlaced_inquiry_scan", - "supports_reading_remote_extended_features", - ], - "bluetooth_ll": [ - "lld_pdu_", - "ld_acl_", - "lld_stop_ind_handler", - "lld_evt_winsize_change", - "config_lld_evt_funcs_reset", - "config_lld_funcs_reset", - "config_llm_funcs_reset", - "llm_set_long_adv_data", - "lld_retry_tx_prog", - "llc_link_sup_to_ind_handler", - "config_llc_funcs_reset", - "lld_evt_rxwin_compute", - "config_btdm_funcs_reset", - "config_ea_funcs_reset", - "llc_defalut_state_tab_reset", - "config_rwip_funcs_reset", - "ke_lmp_rx_flooding_detect", - ], -} - -# Demangled patterns: patterns found in demangled C++ names -DEMANGLED_PATTERNS = { - "gpio_driver": ["GPIO"], - "uart_driver": ["UART"], - "network_stack": [ - "lwip", - "tcp", - "udp", - "ip4", - "ip6", - "dhcp", - "dns", - "netif", - "ethernet", - "ppp", - "slip", - ], - "wifi_stack": ["NetworkInterface"], - "nimble_bt": [ - "nimble", - "NimBLE", - "ble_hs", - "ble_gap", - "ble_gatt", - "ble_att", - "ble_l2cap", - "ble_sm", - ], - "crypto": ["mbedtls", "crypto", "sha", "aes", "rsa", "ecc", "tls", "ssl"], - "cpp_stdlib": ["std::", "__gnu_cxx::", "__cxxabiv"], - "static_init": ["__static_initialization"], - "rtti": ["__type_info", "__class_type_info"], - "web_server_lib": ["AsyncWebServer", "AsyncWebHandler", "WebServer"], - "async_tcp": ["AsyncClient", "AsyncServer"], - "mdns_lib": ["mdns"], - "json_lib": [ - "ArduinoJson", - "JsonDocument", - "JsonArray", - "JsonObject", - "deserialize", - "serialize", - ], - "http_lib": ["HTTP", "http_", "Request", "Response", "Uri", "WebSocket"], - "logging": ["log", "Log", "print", "Print", "diag_"], - "authentication": ["checkDigestAuthentication"], - "libgcc": ["libgcc"], - "esp_system": ["esp_", "ESP"], - "arduino": ["arduino"], - "nvs": ["nvs_", "_ZTVN3nvs", "nvs::"], - "filesystem": ["spiffs", "vfs"], - "libc": ["newlib"], -} - - -# Get the list of actual ESPHome components by scanning the components directory -def get_esphome_components(): - """Get set of actual ESPHome components from the components directory.""" - components = set() - - # Find the components directory relative to this file - current_dir = Path(__file__).parent - components_dir = current_dir / "components" - - if components_dir.exists() and components_dir.is_dir(): - for item in components_dir.iterdir(): - if ( - item.is_dir() - and not item.name.startswith(".") - and not item.name.startswith("__") - ): - components.add(item.name) - - return components - - -# Cache the component list -ESPHOME_COMPONENTS = get_esphome_components() - - -class MemorySection: - """Represents a memory section with its symbols.""" - - def __init__(self, name: str): - self.name = name - self.symbols: list[tuple[str, int, str]] = [] # (symbol_name, size, component) - self.total_size = 0 - - -class ComponentMemory: - """Tracks memory usage for a component.""" - - def __init__(self, name: str): - self.name = name - self.text_size = 0 # Code in flash - self.rodata_size = 0 # Read-only data in flash - self.data_size = 0 # Initialized data (flash + ram) - self.bss_size = 0 # Uninitialized data (ram only) - self.symbol_count = 0 - - @property - def flash_total(self) -> int: - return self.text_size + self.rodata_size + self.data_size - - @property - def ram_total(self) -> int: - return self.data_size + self.bss_size - - -class MemoryAnalyzer: - """Analyzes memory usage from ELF files.""" - - def __init__( - self, - elf_path: str, - objdump_path: str | None = None, - readelf_path: str | None = None, - external_components: set[str] | None = None, - ): - self.elf_path = Path(elf_path) - if not self.elf_path.exists(): - raise FileNotFoundError(f"ELF file not found: {elf_path}") - - self.objdump_path = objdump_path or "objdump" - self.readelf_path = readelf_path or "readelf" - self.external_components = external_components or set() - - self.sections: dict[str, MemorySection] = {} - self.components: dict[str, ComponentMemory] = defaultdict( - lambda: ComponentMemory("") - ) - self._demangle_cache: dict[str, str] = {} - self._uncategorized_symbols: list[tuple[str, str, int]] = [] - self._esphome_core_symbols: list[ - tuple[str, str, int] - ] = [] # Track core symbols - self._component_symbols: dict[str, list[tuple[str, str, int]]] = defaultdict( - list - ) # Track symbols for all components - - def analyze(self) -> dict[str, ComponentMemory]: - """Analyze the ELF file and return component memory usage.""" - self._parse_sections() - self._parse_symbols() - self._categorize_symbols() - return dict(self.components) - - def _parse_sections(self) -> None: - """Parse section headers from ELF file.""" - try: - result = subprocess.run( - [self.readelf_path, "-S", str(self.elf_path)], - capture_output=True, - text=True, - check=True, - ) - - # Parse section headers - for line in result.stdout.splitlines(): - # Look for section entries - match = re.match( - r"\s*\[\s*\d+\]\s+([\.\w]+)\s+\w+\s+[\da-fA-F]+\s+[\da-fA-F]+\s+([\da-fA-F]+)", - line, - ) - if match: - section_name = match.group(1) - size_hex = match.group(2) - size = int(size_hex, 16) - - # Map various section names to standard categories - mapped_section = None - if ".text" in section_name or ".iram" in section_name: - mapped_section = ".text" - elif ".rodata" in section_name: - mapped_section = ".rodata" - elif ".data" in section_name and "bss" not in section_name: - mapped_section = ".data" - elif ".bss" in section_name: - mapped_section = ".bss" - - if mapped_section: - if mapped_section not in self.sections: - self.sections[mapped_section] = MemorySection( - mapped_section - ) - self.sections[mapped_section].total_size += size - - except subprocess.CalledProcessError as e: - _LOGGER.error(f"Failed to parse sections: {e}") - raise - - def _parse_symbols(self) -> None: - """Parse symbols from ELF file.""" - # Section mapping - centralizes the logic - SECTION_MAPPING = { - ".text": [".text", ".iram"], - ".rodata": [".rodata"], - ".data": [".data", ".dram"], - ".bss": [".bss"], - } - - def map_section_name(raw_section: str) -> str | None: - """Map raw section name to standard section.""" - for standard_section, patterns in SECTION_MAPPING.items(): - if any(pattern in raw_section for pattern in patterns): - return standard_section - return None - - def parse_symbol_line(line: str) -> tuple[str, str, int, str] | None: - """Parse a single symbol line from objdump output. - - Returns (section, name, size, address) or None if not a valid symbol. - Format: address l/g w/d F/O section size name - Example: 40084870 l F .iram0.text 00000000 _xt_user_exc - """ - parts = line.split() - if len(parts) < 5: - return None - - try: - # Validate and extract address - address = parts[0] - int(address, 16) - except ValueError: - return None - - # Look for F (function) or O (object) flag - if "F" not in parts and "O" not in parts: - return None - - # Find section, size, and name - for i, part in enumerate(parts): - if part.startswith("."): - section = map_section_name(part) - if section and i + 1 < len(parts): - try: - size = int(parts[i + 1], 16) - if i + 2 < len(parts) and size > 0: - name = " ".join(parts[i + 2 :]) - return (section, name, size, address) - except ValueError: - pass - break - return None - - try: - result = subprocess.run( - [self.objdump_path, "-t", str(self.elf_path)], - capture_output=True, - text=True, - check=True, - ) - - # Track seen addresses to avoid duplicates - seen_addresses: set[str] = set() - - for line in result.stdout.splitlines(): - symbol_info = parse_symbol_line(line) - if symbol_info: - section, name, size, address = symbol_info - # Skip duplicate symbols at the same address (e.g., C1/C2 constructors) - if address not in seen_addresses and section in self.sections: - self.sections[section].symbols.append((name, size, "")) - seen_addresses.add(address) - - except subprocess.CalledProcessError as e: - _LOGGER.error(f"Failed to parse symbols: {e}") - raise - - def _categorize_symbols(self) -> None: - """Categorize symbols by component.""" - # First, collect all unique symbol names for batch demangling - all_symbols = set() - for section in self.sections.values(): - for symbol_name, _, _ in section.symbols: - all_symbols.add(symbol_name) - - # Batch demangle all symbols at once - self._batch_demangle_symbols(list(all_symbols)) - - # Now categorize with cached demangled names - for section_name, section in self.sections.items(): - for symbol_name, size, _ in section.symbols: - component = self._identify_component(symbol_name) - - if component not in self.components: - self.components[component] = ComponentMemory(component) - - comp_mem = self.components[component] - comp_mem.symbol_count += 1 - - if section_name == ".text": - comp_mem.text_size += size - elif section_name == ".rodata": - comp_mem.rodata_size += size - elif section_name == ".data": - comp_mem.data_size += size - elif section_name == ".bss": - comp_mem.bss_size += size - - # Track uncategorized symbols - if component == "other" and size > 0: - demangled = self._demangle_symbol(symbol_name) - self._uncategorized_symbols.append((symbol_name, demangled, size)) - - # Track ESPHome core symbols for detailed analysis - if component == "[esphome]core" and size > 0: - demangled = self._demangle_symbol(symbol_name) - self._esphome_core_symbols.append((symbol_name, demangled, size)) - - # Track all component symbols for detailed analysis - if size > 0: - demangled = self._demangle_symbol(symbol_name) - self._component_symbols[component].append( - (symbol_name, demangled, size) - ) - - def _identify_component(self, symbol_name: str) -> str: - """Identify which component a symbol belongs to.""" - # Demangle C++ names if needed - demangled = self._demangle_symbol(symbol_name) - - # Check for special component classes first (before namespace pattern) - # This handles cases like esphome::ESPHomeOTAComponent which should map to ota - if "esphome::" in demangled: - # Check for special component classes that include component name in the class - # For example: esphome::ESPHomeOTAComponent -> ota component - for component_name in ESPHOME_COMPONENTS: - # Check various naming patterns - component_upper = component_name.upper() - component_camel = component_name.replace("_", "").title() - patterns = [ - f"esphome::{component_upper}Component", # e.g., esphome::OTAComponent - f"esphome::ESPHome{component_upper}Component", # e.g., esphome::ESPHomeOTAComponent - f"esphome::{component_camel}Component", # e.g., esphome::OtaComponent - f"esphome::ESPHome{component_camel}Component", # e.g., esphome::ESPHomeOtaComponent - ] - - if any(pattern in demangled for pattern in patterns): - return f"[esphome]{component_name}" - - # Check for ESPHome component namespaces - match = ESPHOME_COMPONENT_PATTERN.search(demangled) - if match: - component_name = match.group(1) - # Strip trailing underscore if present (e.g., switch_ -> switch) - component_name = component_name.rstrip("_") - - # Check if this is an actual component in the components directory - if component_name in ESPHOME_COMPONENTS: - return f"[esphome]{component_name}" - # Check if this is a known external component from the config - elif component_name in self.external_components: - return f"[external]{component_name}" - else: - # Everything else in esphome:: namespace is core - return "[esphome]core" - - # Check for esphome core namespace (no component namespace) - if "esphome::" in demangled: - # If no component match found, it's core - return "[esphome]core" - - # Check against symbol patterns - for component, patterns in SYMBOL_PATTERNS.items(): - if any(pattern in symbol_name for pattern in patterns): - return component - - # Check against demangled patterns - for component, patterns in DEMANGLED_PATTERNS.items(): - if any(pattern in demangled for pattern in patterns): - return component - - # Special cases that need more complex logic - - # Check if spi_flash vs spi_driver - if "spi_" in symbol_name or "SPI" in symbol_name: - if "spi_flash" in symbol_name: - return "spi_flash" - else: - return "spi_driver" - - # libc special printf variants - if symbol_name.startswith("_") and symbol_name[1:].replace("_r", "").replace( - "v", "" - ).replace("s", "") in ["printf", "fprintf", "sprintf", "scanf"]: - return "libc" - - # Track uncategorized symbols for analysis - return "other" - - def _batch_demangle_symbols(self, symbols: list[str]) -> None: - """Batch demangle C++ symbol names for efficiency.""" - if not symbols: - return - - # Try to find the appropriate c++filt for the platform - cppfilt_cmd = "c++filt" - - # Check if we have a toolchain-specific c++filt - if self.objdump_path and self.objdump_path != "objdump": - # Replace objdump with c++filt in the path - potential_cppfilt = self.objdump_path.replace("objdump", "c++filt") - if Path(potential_cppfilt).exists(): - cppfilt_cmd = potential_cppfilt - - try: - # Send all symbols to c++filt at once - result = subprocess.run( - [cppfilt_cmd], - input="\n".join(symbols), - capture_output=True, - text=True, - check=False, - ) - if result.returncode == 0: - demangled_lines = result.stdout.strip().split("\n") - # Map original to demangled names - for original, demangled in zip(symbols, demangled_lines): - self._demangle_cache[original] = demangled - else: - # If batch fails, cache originals - for symbol in symbols: - self._demangle_cache[symbol] = symbol - except Exception: - # On error, cache originals - for symbol in symbols: - self._demangle_cache[symbol] = symbol - - def _demangle_symbol(self, symbol: str) -> str: - """Get demangled C++ symbol name from cache.""" - return self._demangle_cache.get(symbol, symbol) - - def _categorize_esphome_core_symbol(self, demangled: str) -> str: - """Categorize ESPHome core symbols into subcategories.""" - # Dictionary of patterns for core subcategories - CORE_SUBCATEGORY_PATTERNS = { - "Component Framework": ["Component"], - "Application Core": ["Application"], - "Scheduler": ["Scheduler"], - "Logging": ["Logger", "log_"], - "Preferences": ["preferences", "Preferences"], - "Synchronization": ["Mutex", "Lock"], - "Helpers": ["Helper"], - "Network Utilities": ["network", "Network"], - "Time Management": ["time", "Time"], - "String Utilities": ["str_", "string"], - "Parsing/Formatting": ["parse_", "format_"], - "Optional Types": ["optional", "Optional"], - "Callbacks": ["Callback", "callback"], - "Color Utilities": ["Color"], - "C++ Operators": ["operator"], - "Global Variables": ["global_", "_GLOBAL"], - "Setup/Loop": ["setup", "loop"], - "System Control": ["reboot", "restart"], - "GPIO Management": ["GPIO", "gpio"], - "Interrupt Handling": ["ISR", "interrupt"], - "Hooks": ["Hook", "hook"], - "Entity Base Classes": ["Entity"], - "Automation Framework": ["automation", "Automation"], - "Automation Components": ["Condition", "Action", "Trigger"], - "Lambda Support": ["lambda"], - } - - # Special patterns that need to be checked separately - if any(pattern in demangled for pattern in ["vtable", "typeinfo", "thunk"]): - return "C++ Runtime (vtables/RTTI)" - - if demangled.startswith("std::"): - return "C++ STL" - - # Check against patterns - for category, patterns in CORE_SUBCATEGORY_PATTERNS.items(): - if any(pattern in demangled for pattern in patterns): - return category - - return "Other Core" - - def generate_report(self, detailed: bool = False) -> str: - """Generate a formatted memory report.""" - components = sorted( - self.components.items(), key=lambda x: x[1].flash_total, reverse=True - ) - - # Calculate totals - total_flash = sum(c.flash_total for _, c in components) - total_ram = sum(c.ram_total for _, c in components) - - # Build report - lines = [] - - # Column width constants - COL_COMPONENT = 29 - COL_FLASH_TEXT = 14 - COL_FLASH_DATA = 14 - COL_RAM_DATA = 12 - COL_RAM_BSS = 12 - COL_TOTAL_FLASH = 15 - COL_TOTAL_RAM = 12 - COL_SEPARATOR = 3 # " | " - - # Core analysis column widths - COL_CORE_SUBCATEGORY = 30 - COL_CORE_SIZE = 12 - COL_CORE_COUNT = 6 - COL_CORE_PERCENT = 10 - - # Calculate the exact table width - table_width = ( - COL_COMPONENT - + COL_SEPARATOR - + COL_FLASH_TEXT - + COL_SEPARATOR - + COL_FLASH_DATA - + COL_SEPARATOR - + COL_RAM_DATA - + COL_SEPARATOR - + COL_RAM_BSS - + COL_SEPARATOR - + COL_TOTAL_FLASH - + COL_SEPARATOR - + COL_TOTAL_RAM - ) - - lines.append("=" * table_width) - lines.append("Component Memory Analysis".center(table_width)) - lines.append("=" * table_width) - lines.append("") - - # Main table - fixed column widths - lines.append( - f"{'Component':<{COL_COMPONENT}} | {'Flash (text)':>{COL_FLASH_TEXT}} | {'Flash (data)':>{COL_FLASH_DATA}} | {'RAM (data)':>{COL_RAM_DATA}} | {'RAM (bss)':>{COL_RAM_BSS}} | {'Total Flash':>{COL_TOTAL_FLASH}} | {'Total RAM':>{COL_TOTAL_RAM}}" - ) - lines.append( - "-" * COL_COMPONENT - + "-+-" - + "-" * COL_FLASH_TEXT - + "-+-" - + "-" * COL_FLASH_DATA - + "-+-" - + "-" * COL_RAM_DATA - + "-+-" - + "-" * COL_RAM_BSS - + "-+-" - + "-" * COL_TOTAL_FLASH - + "-+-" - + "-" * COL_TOTAL_RAM - ) - - for name, mem in components: - if mem.flash_total > 0 or mem.ram_total > 0: - flash_rodata = mem.rodata_size + mem.data_size - lines.append( - f"{name:<{COL_COMPONENT}} | {mem.text_size:>{COL_FLASH_TEXT - 2},} B | {flash_rodata:>{COL_FLASH_DATA - 2},} B | " - f"{mem.data_size:>{COL_RAM_DATA - 2},} B | {mem.bss_size:>{COL_RAM_BSS - 2},} B | " - f"{mem.flash_total:>{COL_TOTAL_FLASH - 2},} B | {mem.ram_total:>{COL_TOTAL_RAM - 2},} B" - ) - - lines.append( - "-" * COL_COMPONENT - + "-+-" - + "-" * COL_FLASH_TEXT - + "-+-" - + "-" * COL_FLASH_DATA - + "-+-" - + "-" * COL_RAM_DATA - + "-+-" - + "-" * COL_RAM_BSS - + "-+-" - + "-" * COL_TOTAL_FLASH - + "-+-" - + "-" * COL_TOTAL_RAM - ) - lines.append( - f"{'TOTAL':<{COL_COMPONENT}} | {' ':>{COL_FLASH_TEXT}} | {' ':>{COL_FLASH_DATA}} | " - f"{' ':>{COL_RAM_DATA}} | {' ':>{COL_RAM_BSS}} | " - f"{total_flash:>{COL_TOTAL_FLASH - 2},} B | {total_ram:>{COL_TOTAL_RAM - 2},} B" - ) - - # Top consumers - lines.append("") - lines.append("Top Flash Consumers:") - for i, (name, mem) in enumerate(components[:10]): - if mem.flash_total > 0: - percentage = ( - (mem.flash_total / total_flash * 100) if total_flash > 0 else 0 - ) - lines.append( - f"{i + 1}. {name} ({mem.flash_total:,} B) - {percentage:.1f}% of analyzed flash" - ) - - lines.append("") - lines.append("Top RAM Consumers:") - ram_components = sorted(components, key=lambda x: x[1].ram_total, reverse=True) - for i, (name, mem) in enumerate(ram_components[:10]): - if mem.ram_total > 0: - percentage = (mem.ram_total / total_ram * 100) if total_ram > 0 else 0 - lines.append( - f"{i + 1}. {name} ({mem.ram_total:,} B) - {percentage:.1f}% of analyzed RAM" - ) - - lines.append("") - lines.append( - "Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included." - ) - lines.append("=" * table_width) - - # Add ESPHome core detailed analysis if there are core symbols - if self._esphome_core_symbols: - lines.append("") - lines.append("=" * table_width) - lines.append("[esphome]core Detailed Analysis".center(table_width)) - lines.append("=" * table_width) - lines.append("") - - # Group core symbols by subcategory - core_subcategories: dict[str, list[tuple[str, str, int]]] = defaultdict( - list - ) - - for symbol, demangled, size in self._esphome_core_symbols: - # Categorize based on demangled name patterns - subcategory = self._categorize_esphome_core_symbol(demangled) - core_subcategories[subcategory].append((symbol, demangled, size)) - - # Sort subcategories by total size - sorted_subcategories = sorted( - [ - (name, symbols, sum(s[2] for s in symbols)) - for name, symbols in core_subcategories.items() - ], - key=lambda x: x[2], - reverse=True, - ) - - lines.append( - f"{'Subcategory':<{COL_CORE_SUBCATEGORY}} | {'Size':>{COL_CORE_SIZE}} | " - f"{'Count':>{COL_CORE_COUNT}} | {'% of Core':>{COL_CORE_PERCENT}}" - ) - lines.append( - "-" * COL_CORE_SUBCATEGORY - + "-+-" - + "-" * COL_CORE_SIZE - + "-+-" - + "-" * COL_CORE_COUNT - + "-+-" - + "-" * COL_CORE_PERCENT - ) - - core_total = sum(size for _, _, size in self._esphome_core_symbols) - - for subcategory, symbols, total_size in sorted_subcategories: - percentage = (total_size / core_total * 100) if core_total > 0 else 0 - lines.append( - f"{subcategory:<{COL_CORE_SUBCATEGORY}} | {total_size:>{COL_CORE_SIZE - 2},} B | " - f"{len(symbols):>{COL_CORE_COUNT}} | {percentage:>{COL_CORE_PERCENT - 1}.1f}%" - ) - - # Top 10 largest core symbols - lines.append("") - lines.append("Top 10 Largest [esphome]core Symbols:") - sorted_core_symbols = sorted( - self._esphome_core_symbols, key=lambda x: x[2], reverse=True - ) - - for i, (symbol, demangled, size) in enumerate(sorted_core_symbols[:10]): - lines.append(f"{i + 1}. {demangled} ({size:,} B)") - - lines.append("=" * table_width) - - # Add detailed analysis for top 5 ESPHome components - esphome_components = [ - (name, mem) - for name, mem in components - if name.startswith("[esphome]") and name != "[esphome]core" - ] - top_esphome_components = sorted( - esphome_components, key=lambda x: x[1].flash_total, reverse=True - )[:5] - - # Check if API component exists and ensure it's included - api_component = None - for name, mem in components: - if name == "[esphome]api": - api_component = (name, mem) - break - - # If API exists and not in top 5, add it to the list - components_to_analyze = list(top_esphome_components) - if api_component and api_component not in components_to_analyze: - components_to_analyze.append(api_component) - - if components_to_analyze: - for comp_name, comp_mem in components_to_analyze: - comp_symbols = self._component_symbols.get(comp_name, []) - if comp_symbols: - lines.append("") - lines.append("=" * table_width) - lines.append(f"{comp_name} Detailed Analysis".center(table_width)) - lines.append("=" * table_width) - lines.append("") - - # Sort symbols by size - sorted_symbols = sorted( - comp_symbols, key=lambda x: x[2], reverse=True - ) - - lines.append(f"Total symbols: {len(sorted_symbols)}") - lines.append(f"Total size: {comp_mem.flash_total:,} B") - lines.append("") - - # For API component, show all symbols; for others show top 10 - if comp_name == "[esphome]api": - lines.append(f"All {comp_name} Symbols (sorted by size):") - for i, (symbol, demangled, size) in enumerate(sorted_symbols): - lines.append(f"{i + 1}. {demangled} ({size:,} B)") - else: - lines.append(f"Top 10 Largest {comp_name} Symbols:") - for i, (symbol, demangled, size) in enumerate( - sorted_symbols[:10] - ): - lines.append(f"{i + 1}. {demangled} ({size:,} B)") - - lines.append("=" * table_width) - - return "\n".join(lines) - - def to_json(self) -> str: - """Export analysis results as JSON.""" - data = { - "components": { - name: { - "text": mem.text_size, - "rodata": mem.rodata_size, - "data": mem.data_size, - "bss": mem.bss_size, - "flash_total": mem.flash_total, - "ram_total": mem.ram_total, - "symbol_count": mem.symbol_count, - } - for name, mem in self.components.items() - }, - "totals": { - "flash": sum(c.flash_total for c in self.components.values()), - "ram": sum(c.ram_total for c in self.components.values()), - }, - } - return json.dumps(data, indent=2) - - def dump_uncategorized_symbols(self, output_file: str | None = None) -> None: - """Dump uncategorized symbols for analysis.""" - # Sort by size descending - sorted_symbols = sorted( - self._uncategorized_symbols, key=lambda x: x[2], reverse=True - ) - - lines = ["Uncategorized Symbols Analysis", "=" * 80] - lines.append(f"Total uncategorized symbols: {len(sorted_symbols)}") - lines.append( - f"Total uncategorized size: {sum(s[2] for s in sorted_symbols):,} bytes" - ) - lines.append("") - lines.append(f"{'Size':>10} | {'Symbol':<60} | Demangled") - lines.append("-" * 10 + "-+-" + "-" * 60 + "-+-" + "-" * 40) - - for symbol, demangled, size in sorted_symbols[:100]: # Top 100 - if symbol != demangled: - lines.append(f"{size:>10,} | {symbol[:60]:<60} | {demangled[:100]}") - else: - lines.append(f"{size:>10,} | {symbol[:60]:<60} | [not demangled]") - - if len(sorted_symbols) > 100: - lines.append(f"\n... and {len(sorted_symbols) - 100} more symbols") - - content = "\n".join(lines) - - if output_file: - with open(output_file, "w") as f: - f.write(content) - else: - print(content) - - -def analyze_elf( - elf_path: str, - objdump_path: str | None = None, - readelf_path: str | None = None, - detailed: bool = False, - external_components: set[str] | None = None, -) -> str: - """Analyze an ELF file and return a memory report.""" - analyzer = MemoryAnalyzer(elf_path, objdump_path, readelf_path, external_components) - analyzer.analyze() - return analyzer.generate_report(detailed) - - -if __name__ == "__main__": - import sys - - if len(sys.argv) < 2: - print("Usage: analyze_memory.py ") - sys.exit(1) - - try: - report = analyze_elf(sys.argv[1]) - print(report) - except Exception as e: - print(f"Error: {e}") - sys.exit(1) From 887e69e0b22fa36e7c96d44ecbc39902bbf061cb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Oct 2025 11:24:03 -0500 Subject: [PATCH 146/394] merge --- .../template/binary_sensor/__init__.py | 8 ++- .../binary_sensor/template_binary_sensor.cpp | 4 +- .../binary_sensor/template_binary_sensor.h | 4 +- .../template/cover/template_cover.cpp | 4 +- .../template/cover/template_cover.h | 8 +-- .../template/datetime/template_date.h | 4 +- .../template/datetime/template_datetime.h | 4 +- .../template/datetime/template_time.h | 4 +- .../template/lock/template_lock.cpp | 2 +- .../components/template/lock/template_lock.h | 4 +- .../template/number/template_number.h | 4 +- .../template/select/template_select.h | 4 +- .../template/sensor/template_sensor.cpp | 2 +- .../template/sensor/template_sensor.h | 4 +- .../template/switch/template_switch.cpp | 2 +- .../template/switch/template_switch.h | 4 +- .../components/template/text/template_text.h | 4 +- .../text_sensor/template_text_sensor.cpp | 2 +- .../text_sensor/template_text_sensor.h | 4 +- .../template/valve/template_valve.cpp | 2 +- .../template/valve/template_valve.h | 4 +- esphome/cpp_generator.py | 8 +++ tests/component_tests/text/test_text.py | 5 +- tests/unit_tests/test_cpp_generator.py | 55 +++++++++++++++++++ 24 files changed, 110 insertions(+), 40 deletions(-) diff --git a/esphome/components/template/binary_sensor/__init__.py b/esphome/components/template/binary_sensor/__init__.py index c93876380d..9d4208dcca 100644 --- a/esphome/components/template/binary_sensor/__init__.py +++ b/esphome/components/template/binary_sensor/__init__.py @@ -38,8 +38,14 @@ async def to_code(config): condition = await automation.build_condition( condition, cg.TemplateArguments(), [] ) + # Generate a stateless lambda that calls condition.check() + # capture="" is safe because condition is a global variable in generated C++ code + # and doesn't need to be captured. This allows implicit conversion to function pointer. template_ = LambdaExpression( - f"return {condition.check()};", [], return_type=cg.optional.template(bool) + f"return {condition.check()};", + [], + return_type=cg.optional.template(bool), + capture="", ) cg.add(var.set_template(template_)) diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.cpp b/esphome/components/template/binary_sensor/template_binary_sensor.cpp index d1fb618695..8543dff4dc 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.cpp +++ b/esphome/components/template/binary_sensor/template_binary_sensor.cpp @@ -9,10 +9,10 @@ static const char *const TAG = "template.binary_sensor"; void TemplateBinarySensor::setup() { this->loop(); } void TemplateBinarySensor::loop() { - if (this->f_ == nullptr) + if (!this->f_.has_value()) return; - auto s = this->f_(); + auto s = (*this->f_)(); if (s.has_value()) { this->publish_state(*s); } diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.h b/esphome/components/template/binary_sensor/template_binary_sensor.h index 5e5624d82e..2e0b216eb4 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.h +++ b/esphome/components/template/binary_sensor/template_binary_sensor.h @@ -8,7 +8,7 @@ namespace template_ { class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void loop() override; @@ -17,7 +17,7 @@ class TemplateBinarySensor : public Component, public binary_sensor::BinarySenso float get_setup_priority() const override { return setup_priority::HARDWARE; } protected: - std::function()> f_{nullptr}; + optional (*)()> f_; }; } // namespace template_ diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index 84c687536e..bed3931e78 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -63,7 +63,7 @@ void TemplateCover::loop() { } void TemplateCover::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void TemplateCover::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } -void TemplateCover::set_state_lambda(std::function()> &&f) { this->state_f_ = f; } +void TemplateCover::set_state_lambda(optional (*f)()) { this->state_f_ = f; } float TemplateCover::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; } Trigger<> *TemplateCover::get_close_trigger() const { return this->close_trigger_; } @@ -124,7 +124,7 @@ CoverTraits TemplateCover::get_traits() { } Trigger *TemplateCover::get_position_trigger() const { return this->position_trigger_; } Trigger *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; } -void TemplateCover::set_tilt_lambda(std::function()> &&tilt_f) { this->tilt_f_ = tilt_f; } +void TemplateCover::set_tilt_lambda(optional (*tilt_f)()) { this->tilt_f_ = tilt_f; } void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } void TemplateCover::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index 958c94b0a6..ed1ebf4e43 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -17,7 +17,7 @@ class TemplateCover : public cover::Cover, public Component { public: TemplateCover(); - void set_state_lambda(std::function()> &&f); + void set_state_lambda(optional (*f)()); Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; @@ -26,7 +26,7 @@ class TemplateCover : public cover::Cover, public Component { Trigger *get_tilt_trigger() const; void set_optimistic(bool optimistic); void set_assumed_state(bool assumed_state); - void set_tilt_lambda(std::function()> &&tilt_f); + void set_tilt_lambda(optional (*tilt_f)()); void set_has_stop(bool has_stop); void set_has_position(bool has_position); void set_has_tilt(bool has_tilt); @@ -45,8 +45,8 @@ class TemplateCover : public cover::Cover, public Component { void stop_prev_trigger_(); TemplateCoverRestoreMode restore_mode_{COVER_RESTORE}; - optional()>> state_f_; - optional()>> tilt_f_; + optional (*)()> state_f_; + optional (*)()> tilt_f_; bool assumed_state_{false}; bool optimistic_{false}; Trigger<> *open_trigger_; diff --git a/esphome/components/template/datetime/template_date.h b/esphome/components/template/datetime/template_date.h index 185c7ed49d..2a0967fc94 100644 --- a/esphome/components/template/datetime/template_date.h +++ b/esphome/components/template/datetime/template_date.h @@ -15,7 +15,7 @@ namespace template_ { class TemplateDate : public datetime::DateEntity, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void update() override; @@ -35,7 +35,7 @@ class TemplateDate : public datetime::DateEntity, public PollingComponent { ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + optional (*)()> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/datetime/template_datetime.h b/esphome/components/template/datetime/template_datetime.h index ef80ded89a..d917015b67 100644 --- a/esphome/components/template/datetime/template_datetime.h +++ b/esphome/components/template/datetime/template_datetime.h @@ -15,7 +15,7 @@ namespace template_ { class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void update() override; @@ -35,7 +35,7 @@ class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponen ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + optional (*)()> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/datetime/template_time.h b/esphome/components/template/datetime/template_time.h index 4a7c0098ec..2f05ba0737 100644 --- a/esphome/components/template/datetime/template_time.h +++ b/esphome/components/template/datetime/template_time.h @@ -15,7 +15,7 @@ namespace template_ { class TemplateTime : public datetime::TimeEntity, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void update() override; @@ -35,7 +35,7 @@ class TemplateTime : public datetime::TimeEntity, public PollingComponent { ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + optional (*)()> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/lock/template_lock.cpp b/esphome/components/template/lock/template_lock.cpp index 87ba1046eb..c2e227c26d 100644 --- a/esphome/components/template/lock/template_lock.cpp +++ b/esphome/components/template/lock/template_lock.cpp @@ -45,7 +45,7 @@ void TemplateLock::open_latch() { this->open_trigger_->trigger(); } void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } -void TemplateLock::set_state_lambda(std::function()> &&f) { this->f_ = f; } +void TemplateLock::set_state_lambda(optional (*f)()) { this->f_ = f; } float TemplateLock::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateLock::get_lock_trigger() const { return this->lock_trigger_; } Trigger<> *TemplateLock::get_unlock_trigger() const { return this->unlock_trigger_; } diff --git a/esphome/components/template/lock/template_lock.h b/esphome/components/template/lock/template_lock.h index 4f798eca81..428744a66f 100644 --- a/esphome/components/template/lock/template_lock.h +++ b/esphome/components/template/lock/template_lock.h @@ -13,7 +13,7 @@ class TemplateLock : public lock::Lock, public Component { void dump_config() override; - void set_state_lambda(std::function()> &&f); + void set_state_lambda(optional (*f)()); Trigger<> *get_lock_trigger() const; Trigger<> *get_unlock_trigger() const; Trigger<> *get_open_trigger() const; @@ -26,7 +26,7 @@ class TemplateLock : public lock::Lock, public Component { void control(const lock::LockCall &call) override; void open_latch() override; - optional()>> f_; + optional (*)()> f_; bool optimistic_{false}; Trigger<> *lock_trigger_; Trigger<> *unlock_trigger_; diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h index 9a82e44339..e77b181d25 100644 --- a/esphome/components/template/number/template_number.h +++ b/esphome/components/template/number/template_number.h @@ -10,7 +10,7 @@ namespace template_ { class TemplateNumber : public number::Number, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void update() override; @@ -28,7 +28,7 @@ class TemplateNumber : public number::Number, public PollingComponent { float initial_value_{NAN}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + optional (*)()> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 2f00765c3d..c1b348b26a 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -10,7 +10,7 @@ namespace template_ { class TemplateSelect : public select::Select, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void update() override; @@ -28,7 +28,7 @@ class TemplateSelect : public select::Select, public PollingComponent { std::string initial_option_; bool restore_value_ = false; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + optional (*)()> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/sensor/template_sensor.cpp b/esphome/components/template/sensor/template_sensor.cpp index f2d0e7363e..65f2417670 100644 --- a/esphome/components/template/sensor/template_sensor.cpp +++ b/esphome/components/template/sensor/template_sensor.cpp @@ -17,7 +17,7 @@ void TemplateSensor::update() { } } float TemplateSensor::get_setup_priority() const { return setup_priority::HARDWARE; } -void TemplateSensor::set_template(std::function()> &&f) { this->f_ = f; } +void TemplateSensor::set_template(optional (*f)()) { this->f_ = f; } void TemplateSensor::dump_config() { LOG_SENSOR("", "Template Sensor", this); LOG_UPDATE_INTERVAL(this); diff --git a/esphome/components/template/sensor/template_sensor.h b/esphome/components/template/sensor/template_sensor.h index 2630cb0b14..369313d607 100644 --- a/esphome/components/template/sensor/template_sensor.h +++ b/esphome/components/template/sensor/template_sensor.h @@ -8,7 +8,7 @@ namespace template_ { class TemplateSensor : public sensor::Sensor, public PollingComponent { public: - void set_template(std::function()> &&f); + void set_template(optional (*f)()); void update() override; @@ -17,7 +17,7 @@ class TemplateSensor : public sensor::Sensor, public PollingComponent { float get_setup_priority() const override; protected: - optional()>> f_; + optional (*)()> f_; }; } // namespace template_ diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index fa236f6364..5aaf514b2a 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -35,7 +35,7 @@ void TemplateSwitch::write_state(bool state) { } void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } bool TemplateSwitch::assumed_state() { return this->assumed_state_; } -void TemplateSwitch::set_state_lambda(std::function()> &&f) { this->f_ = f; } +void TemplateSwitch::set_state_lambda(optional (*f)()) { this->f_ = f; } float TemplateSwitch::get_setup_priority() const { return setup_priority::HARDWARE - 2.0f; } Trigger<> *TemplateSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } Trigger<> *TemplateSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index bfe9ac25d6..0fba66b9bd 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -14,7 +14,7 @@ class TemplateSwitch : public switch_::Switch, public Component { void setup() override; void dump_config() override; - void set_state_lambda(std::function()> &&f); + void set_state_lambda(optional (*f)()); Trigger<> *get_turn_on_trigger() const; Trigger<> *get_turn_off_trigger() const; void set_optimistic(bool optimistic); @@ -28,7 +28,7 @@ class TemplateSwitch : public switch_::Switch, public Component { void write_state(bool state) override; - optional()>> f_; + optional (*)()> f_; bool optimistic_{false}; bool assumed_state_{false}; Trigger<> *turn_on_trigger_; diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index bcfc54a2ba..6c17d2016a 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -61,7 +61,7 @@ template class TextSaver : public TemplateTextSaverBase { class TemplateText : public text::Text, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void update() override; @@ -78,7 +78,7 @@ class TemplateText : public text::Text, public PollingComponent { bool optimistic_ = false; std::string initial_value_; Trigger *set_trigger_ = new Trigger(); - optional()>> f_{nullptr}; + optional (*)()> f_{nullptr}; TemplateTextSaverBase *pref_ = nullptr; }; diff --git a/esphome/components/template/text_sensor/template_text_sensor.cpp b/esphome/components/template/text_sensor/template_text_sensor.cpp index 885ad47bbf..2b0297d62f 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.cpp +++ b/esphome/components/template/text_sensor/template_text_sensor.cpp @@ -16,7 +16,7 @@ void TemplateTextSensor::update() { } } float TemplateTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } -void TemplateTextSensor::set_template(std::function()> &&f) { this->f_ = f; } +void TemplateTextSensor::set_template(optional (*f)()) { this->f_ = f; } void TemplateTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Template Sensor", this); } } // namespace template_ diff --git a/esphome/components/template/text_sensor/template_text_sensor.h b/esphome/components/template/text_sensor/template_text_sensor.h index 07a2bd96fc..48e40c2493 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.h +++ b/esphome/components/template/text_sensor/template_text_sensor.h @@ -9,7 +9,7 @@ namespace template_ { class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent { public: - void set_template(std::function()> &&f); + void set_template(optional (*f)()); void update() override; @@ -18,7 +18,7 @@ class TemplateTextSensor : public text_sensor::TextSensor, public PollingCompone void dump_config() override; protected: - optional()>> f_{}; + optional (*)()> f_{}; }; } // namespace template_ diff --git a/esphome/components/template/valve/template_valve.cpp b/esphome/components/template/valve/template_valve.cpp index 5fa14a2de7..b27cc00968 100644 --- a/esphome/components/template/valve/template_valve.cpp +++ b/esphome/components/template/valve/template_valve.cpp @@ -55,7 +55,7 @@ void TemplateValve::loop() { void TemplateValve::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void TemplateValve::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } -void TemplateValve::set_state_lambda(std::function()> &&f) { this->state_f_ = f; } +void TemplateValve::set_state_lambda(optional (*f)()) { this->state_f_ = f; } float TemplateValve::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateValve::get_open_trigger() const { return this->open_trigger_; } diff --git a/esphome/components/template/valve/template_valve.h b/esphome/components/template/valve/template_valve.h index 5e3fb6aff3..92c32f3487 100644 --- a/esphome/components/template/valve/template_valve.h +++ b/esphome/components/template/valve/template_valve.h @@ -17,7 +17,7 @@ class TemplateValve : public valve::Valve, public Component { public: TemplateValve(); - void set_state_lambda(std::function()> &&f); + void set_state_lambda(optional (*f)()); Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; @@ -42,7 +42,7 @@ class TemplateValve : public valve::Valve, public Component { void stop_prev_trigger_(); TemplateValveRestoreMode restore_mode_{VALVE_NO_RESTORE}; - optional()>> state_f_; + optional (*)()> state_f_; bool assumed_state_{false}; bool optimistic_{false}; Trigger<> *open_trigger_; diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index b2022c7ae6..a2da424e5a 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -198,6 +198,8 @@ class LambdaExpression(Expression): self.return_type = safe_exp(return_type) if return_type is not None else None def __str__(self): + # Stateless lambdas (empty capture) implicitly convert to function pointers + # when assigned to function pointer types - no unary + needed cpp = f"[{self.capture}]({self.parameters})" if self.return_type is not None: cpp += f" -> {self.return_type}" @@ -700,6 +702,12 @@ async def process_lambda( parts[i * 3 + 1] = var parts[i * 3 + 2] = "" + # All id() references are global variables in generated C++ code. + # Global variables should not be captured - they're accessible everywhere. + # Use empty capture instead of capture-by-value. + if capture == "=": + capture = "" + if isinstance(value, ESPHomeDataBase) and value.esp_range is not None: location = value.esp_range.start_mark location.line += value.content_offset diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 75f1c4b88b..1cc31a288b 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -58,7 +58,7 @@ def test_text_config_value_mode_set(generate_main): def test_text_config_lamda_is_set(generate_main): """ - Test if lambda is set for lambda mode + Test if lambda is set for lambda mode (optimized with stateless lambda) """ # Given @@ -66,5 +66,6 @@ def test_text_config_lamda_is_set(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert "it_4->set_template([=]() -> esphome::optional {" in main_cpp + # Stateless lambda optimization: empty capture list allows function pointer conversion + assert "it_4->set_template([]() -> esphome::optional {" in main_cpp assert 'return std::string{"Hello"};' in main_cpp diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 95633ca0c6..2c9f760c8e 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -173,6 +173,61 @@ class TestLambdaExpression: "}" ) + def test_str__stateless_no_return(self): + """Test stateless lambda (empty capture) generates correctly""" + target = cg.LambdaExpression( + ('ESP_LOGD("main", "Test message");',), + (), # No parameters + "", # Empty capture (stateless) + ) + + actual = str(target) + + assert actual == ('[]() {\n ESP_LOGD("main", "Test message");\n}') + + def test_str__stateless_with_return(self): + """Test stateless lambda with return type generates correctly""" + target = cg.LambdaExpression( + ("return global_value > 0;",), + (), # No parameters + "", # Empty capture (stateless) + bool, # Return type + ) + + actual = str(target) + + assert actual == ("[]() -> bool {\n return global_value > 0;\n}") + + def test_str__stateless_with_params(self): + """Test stateless lambda with parameters generates correctly""" + target = cg.LambdaExpression( + ("return foo + bar;",), + ((int, "foo"), (float, "bar")), + "", # Empty capture (stateless) + float, + ) + + actual = str(target) + + assert actual == ( + "[](int32_t foo, float bar) -> float {\n return foo + bar;\n}" + ) + + def test_str__with_capture(self): + """Test lambda with capture generates correctly""" + target = cg.LambdaExpression( + ("return captured_var + x;",), + ((int, "x"),), + "captured_var", # Has capture (not stateless) + int, + ) + + actual = str(target) + + assert actual == ( + "[captured_var](int32_t x) -> int32_t {\n return captured_var + x;\n}" + ) + class TestLiterals: @pytest.mark.parametrize( From 7ceebadca6df60708f14bf9cf30274646936cf1d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Oct 2025 11:58:10 -0500 Subject: [PATCH 147/394] [network] Eliminate runtime string parsing for IP address initialization --- esphome/components/ethernet/__init__.py | 12 ++++---- esphome/components/network/__init__.py | 37 +++++++++++++++++++++++++ esphome/components/wifi/__init__.py | 6 ++-- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 77f70a3630..2f02d227d7 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -14,7 +14,7 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S2, VARIANT_ESP32S3, ) -from esphome.components.network import IPAddress +from esphome.components.network import ip_address_literal from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface import esphome.config_validation as cv from esphome.const import ( @@ -320,11 +320,11 @@ def _final_validate_spi(config): def manual_ip(config): return cg.StructInitializer( ManualIP, - ("static_ip", IPAddress(str(config[CONF_STATIC_IP]))), - ("gateway", IPAddress(str(config[CONF_GATEWAY]))), - ("subnet", IPAddress(str(config[CONF_SUBNET]))), - ("dns1", IPAddress(str(config[CONF_DNS1]))), - ("dns2", IPAddress(str(config[CONF_DNS2]))), + ("static_ip", ip_address_literal(config[CONF_STATIC_IP])), + ("gateway", ip_address_literal(config[CONF_GATEWAY])), + ("subnet", ip_address_literal(config[CONF_SUBNET])), + ("dns1", ip_address_literal(config[CONF_DNS1])), + ("dns2", ip_address_literal(config[CONF_DNS2])), ) diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 1a74350c4c..22fc81d2df 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -1,3 +1,5 @@ +import ipaddress + import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option import esphome.config_validation as cv @@ -10,6 +12,41 @@ AUTO_LOAD = ["mdns"] network_ns = cg.esphome_ns.namespace("network") IPAddress = network_ns.class_("IPAddress") + +def ip_address_literal(ip: str | None) -> cg.MockObj: + """Generate an IPAddress with compile-time initialization instead of runtime parsing. + + This function parses the IP address in Python during code generation and generates + a call to the 4-octet constructor (IPAddress(192, 168, 1, 1)) instead of the + string constructor (IPAddress("192.168.1.1")). This eliminates runtime string + parsing overhead and reduces flash usage on embedded systems. + + Args: + ip: IP address as string (e.g., "192.168.1.1"), ipaddress.IPv4Address, or None + + Returns: + IPAddress expression that uses 4-octet constructor for efficiency + """ + if ip is None: + return IPAddress(0, 0, 0, 0) + + try: + # Parse using Python's ipaddress module + ip_obj = ipaddress.ip_address(str(ip)) + except (ValueError, TypeError): + pass + else: + # Only support IPv4 for now + if isinstance(ip_obj, ipaddress.IPv4Address): + # Extract octets from the packed bytes representation + octets = ip_obj.packed + # Generate call to 4-octet constructor: IPAddress(192, 168, 1, 1) + return IPAddress(octets[0], octets[1], octets[2], octets[3]) + + # Fallback to string constructor if parsing fails + return IPAddress(str(ip)) + + CONFIG_SCHEMA = cv.Schema( { cv.SplitDefault( diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index ba488728b7..b980bab4aa 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -3,7 +3,7 @@ from esphome.automation import Condition import esphome.codegen as cg from esphome.components.const import CONF_USE_PSRAM from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant -from esphome.components.network import IPAddress +from esphome.components.network import ip_address_literal 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 @@ -334,9 +334,7 @@ def eap_auth(config): def safe_ip(ip): - if ip is None: - return IPAddress(0, 0, 0, 0) - return IPAddress(str(ip)) + return ip_address_literal(ip) def manual_ip(config): From 6fc96188d5b8b021e5ad7ef607f9d71a6205ff1a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Oct 2025 12:05:30 -0500 Subject: [PATCH 148/394] tweak --- esphome/components/network/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 22fc81d2df..502803da1e 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -13,7 +13,7 @@ network_ns = cg.esphome_ns.namespace("network") IPAddress = network_ns.class_("IPAddress") -def ip_address_literal(ip: str | None) -> cg.MockObj: +def ip_address_literal(ip: str | int | None) -> cg.MockObj: """Generate an IPAddress with compile-time initialization instead of runtime parsing. This function parses the IP address in Python during code generation and generates @@ -32,7 +32,7 @@ def ip_address_literal(ip: str | None) -> cg.MockObj: try: # Parse using Python's ipaddress module - ip_obj = ipaddress.ip_address(str(ip)) + ip_obj = ipaddress.ip_address(ip) except (ValueError, TypeError): pass else: From 3c18558003aab8a4ea193ae043a9847831365220 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Oct 2025 14:06:22 -0500 Subject: [PATCH 149/394] Optimize stateless lambdas to use function pointers (#11551) --- esphome/automation.py | 45 ++++++++++++++-- esphome/components/binary_sensor/__init__.py | 3 +- esphome/components/binary_sensor/filter.h | 15 ++++++ esphome/components/logger/__init__.py | 10 ++-- esphome/components/sensor/__init__.py | 3 +- esphome/components/sensor/filter.h | 15 ++++++ esphome/components/text_sensor/__init__.py | 3 +- esphome/components/text_sensor/filter.h | 15 ++++++ esphome/core/base_automation.h | 25 +++++++++ esphome/cpp_generator.py | 8 +++ tests/component_tests/text/test_text.py | 4 +- tests/unit_tests/test_cpp_generator.py | 55 ++++++++++++++++++++ 12 files changed, 190 insertions(+), 11 deletions(-) diff --git a/esphome/automation.py b/esphome/automation.py index 99def9f273..cfe0af1b59 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -16,7 +16,12 @@ from esphome.const import ( CONF_UPDATE_INTERVAL, ) from esphome.core import ID -from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType +from esphome.cpp_generator import ( + LambdaExpression, + MockObj, + MockObjClass, + TemplateArgsType, +) from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.types import ConfigType from esphome.util import Registry @@ -87,6 +92,7 @@ def validate_potentially_or_condition(value): DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component) LambdaAction = cg.esphome_ns.class_("LambdaAction", Action) +StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action) IfAction = cg.esphome_ns.class_("IfAction", Action) WhileAction = cg.esphome_ns.class_("WhileAction", Action) RepeatAction = cg.esphome_ns.class_("RepeatAction", Action) @@ -97,9 +103,40 @@ ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action) Automation = cg.esphome_ns.class_("Automation") LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition) +StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition) ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component) +def new_lambda_pvariable( + id_obj: ID, + lambda_expr: LambdaExpression, + stateless_class: MockObjClass, + template_arg: cg.TemplateArguments | None = None, +) -> MockObj: + """Create Pvariable for lambda, using stateless class if applicable. + + Combines ID selection and Pvariable creation in one call. For stateless + lambdas (empty capture), uses function pointer instead of std::function. + + Args: + id_obj: The ID object (action_id, condition_id, or filter_id) + lambda_expr: The lambda expression object + stateless_class: The stateless class to use for stateless lambdas + template_arg: Optional template arguments (for actions/conditions) + + Returns: + The created Pvariable + """ + # For stateless lambdas, use function pointer instead of std::function + if lambda_expr.capture == "": + id_obj = id_obj.copy() + id_obj.type = stateless_class + + if template_arg is not None: + return cg.new_Pvariable(id_obj, template_arg, lambda_expr) + return cg.new_Pvariable(id_obj, lambda_expr) + + def validate_automation(extra_schema=None, extra_validators=None, single=False): if extra_schema is None: extra_schema = {} @@ -240,7 +277,9 @@ async def lambda_condition_to_code( args: TemplateArgsType, ) -> MockObj: lambda_ = await cg.process_lambda(config, args, return_type=bool) - return cg.new_Pvariable(condition_id, template_arg, lambda_) + return new_lambda_pvariable( + condition_id, lambda_, StatelessLambdaCondition, template_arg + ) @register_condition( @@ -406,7 +445,7 @@ async def lambda_action_to_code( args: TemplateArgsType, ) -> MockObj: lambda_ = await cg.process_lambda(config, args, return_type=cg.void) - return cg.new_Pvariable(action_id, template_arg, lambda_) + return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg) @register_action( diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 26e784a0b8..8892b57e6e 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -155,6 +155,7 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter) AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component) LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter) +StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter) SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component) _LOGGER = getLogger(__name__) @@ -299,7 +300,7 @@ async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(bool, "x")], return_type=cg.optional.template(bool) ) - return cg.new_Pvariable(filter_id, lambda_) + return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) @register_filter( diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index a7eb080feb..2d473c3b64 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -111,6 +111,21 @@ class LambdaFilter : public Filter { std::function(bool)> f_; }; +/** Optimized lambda filter for stateless lambdas (no capture). + * + * Uses function pointer instead of std::function to reduce memory overhead. + * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). + */ +class StatelessLambdaFilter : public Filter { + public: + explicit StatelessLambdaFilter(optional (*f)(bool)) : f_(f) {} + + optional new_value(bool value) override { return this->f_(value); } + + protected: + optional (*f_)(bool); +}; + class SettleFilter : public Filter, public Component { public: optional new_value(bool value) override; diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 1d02073d27..22bf3d2f4c 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -1,7 +1,7 @@ import re from esphome import automation -from esphome.automation import LambdaAction +from esphome.automation import LambdaAction, StatelessLambdaAction import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant from esphome.components.esp32.const import ( @@ -430,7 +430,9 @@ async def logger_log_action_to_code(config, action_id, template_arg, args): text = str(cg.statement(esp_log(config[CONF_TAG], config[CONF_FORMAT], *args_))) lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) - return cg.new_Pvariable(action_id, template_arg, lambda_) + return automation.new_lambda_pvariable( + action_id, lambda_, StatelessLambdaAction, template_arg + ) @automation.register_action( @@ -455,7 +457,9 @@ async def logger_set_level_to_code(config, action_id, template_arg, args): text = str(cg.statement(logger.set_log_level(level))) lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) - return cg.new_Pvariable(action_id, template_arg, lambda_) + return automation.new_lambda_pvariable( + action_id, lambda_, StatelessLambdaAction, template_arg + ) FILTER_SOURCE_FILES = filter_source_files_from_platform( diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 93283e4d47..41ac3516b9 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -261,6 +261,7 @@ ExponentialMovingAverageFilter = sensor_ns.class_( ) ThrottleAverageFilter = sensor_ns.class_("ThrottleAverageFilter", Filter, cg.Component) LambdaFilter = sensor_ns.class_("LambdaFilter", Filter) +StatelessLambdaFilter = sensor_ns.class_("StatelessLambdaFilter", Filter) OffsetFilter = sensor_ns.class_("OffsetFilter", Filter) MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter) ValueListFilter = sensor_ns.class_("ValueListFilter", Filter) @@ -573,7 +574,7 @@ async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(float, "x")], return_type=cg.optional.template(float) ) - return cg.new_Pvariable(filter_id, lambda_) + return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) DELTA_SCHEMA = cv.Schema( diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index ecd55308d1..75e28a1efe 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -296,6 +296,21 @@ class LambdaFilter : public Filter { lambda_filter_t lambda_filter_; }; +/** Optimized lambda filter for stateless lambdas (no capture). + * + * Uses function pointer instead of std::function to reduce memory overhead. + * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). + */ +class StatelessLambdaFilter : public Filter { + public: + explicit StatelessLambdaFilter(optional (*lambda_filter)(float)) : lambda_filter_(lambda_filter) {} + + optional new_value(float value) override { return this->lambda_filter_(value); } + + protected: + optional (*lambda_filter_)(float); +}; + /// A simple filter that adds `offset` to each value it receives. class OffsetFilter : public Filter { public: diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 7a9e947abd..adc8a76fcd 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -57,6 +57,7 @@ validate_filters = cv.validate_registry("filter", FILTER_REGISTRY) # Filters Filter = text_sensor_ns.class_("Filter") LambdaFilter = text_sensor_ns.class_("LambdaFilter", Filter) +StatelessLambdaFilter = text_sensor_ns.class_("StatelessLambdaFilter", Filter) ToUpperFilter = text_sensor_ns.class_("ToUpperFilter", Filter) ToLowerFilter = text_sensor_ns.class_("ToLowerFilter", Filter) AppendFilter = text_sensor_ns.class_("AppendFilter", Filter) @@ -70,7 +71,7 @@ async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(cg.std_string, "x")], return_type=cg.optional.template(cg.std_string) ) - return cg.new_Pvariable(filter_id, lambda_) + return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) @FILTER_REGISTRY.register("to_upper", ToUpperFilter, {}) diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index c77c221235..85acac5c8d 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -62,6 +62,21 @@ class LambdaFilter : public Filter { lambda_filter_t lambda_filter_; }; +/** Optimized lambda filter for stateless lambdas (no capture). + * + * Uses function pointer instead of std::function to reduce memory overhead. + * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). + */ +class StatelessLambdaFilter : public Filter { + public: + explicit StatelessLambdaFilter(optional (*lambda_filter)(std::string)) : lambda_filter_(lambda_filter) {} + + optional new_value(std::string value) override { return this->lambda_filter_(value); } + + protected: + optional (*lambda_filter_)(std::string); +}; + /// A simple filter that converts all text to uppercase class ToUpperFilter : public Filter { public: diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index af8cde971b..1c60dd1c7a 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -79,6 +79,18 @@ template class LambdaCondition : public Condition { std::function f_; }; +/// Optimized lambda condition for stateless lambdas (no capture). +/// Uses function pointer instead of std::function to reduce memory overhead. +/// Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). +template class StatelessLambdaCondition : public Condition { + public: + explicit StatelessLambdaCondition(bool (*f)(Ts...)) : f_(f) {} + bool check(Ts... x) override { return this->f_(x...); } + + protected: + bool (*f_)(Ts...); +}; + template class ForCondition : public Condition, public Component { public: explicit ForCondition(Condition<> *condition) : condition_(condition) {} @@ -190,6 +202,19 @@ template class LambdaAction : public Action { std::function f_; }; +/// Optimized lambda action for stateless lambdas (no capture). +/// Uses function pointer instead of std::function to reduce memory overhead. +/// Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). +template class StatelessLambdaAction : public Action { + public: + explicit StatelessLambdaAction(void (*f)(Ts...)) : f_(f) {} + + void play(Ts... x) override { this->f_(x...); } + + protected: + void (*f_)(Ts...); +}; + template class IfAction : public Action { public: explicit IfAction(Condition *condition) : condition_(condition) {} diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index b2022c7ae6..a2da424e5a 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -198,6 +198,8 @@ class LambdaExpression(Expression): self.return_type = safe_exp(return_type) if return_type is not None else None def __str__(self): + # Stateless lambdas (empty capture) implicitly convert to function pointers + # when assigned to function pointer types - no unary + needed cpp = f"[{self.capture}]({self.parameters})" if self.return_type is not None: cpp += f" -> {self.return_type}" @@ -700,6 +702,12 @@ async def process_lambda( parts[i * 3 + 1] = var parts[i * 3 + 2] = "" + # All id() references are global variables in generated C++ code. + # Global variables should not be captured - they're accessible everywhere. + # Use empty capture instead of capture-by-value. + if capture == "=": + capture = "" + if isinstance(value, ESPHomeDataBase) and value.esp_range is not None: location = value.esp_range.start_mark location.line += value.content_offset diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 75f1c4b88b..99ddd78ee7 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -58,7 +58,7 @@ def test_text_config_value_mode_set(generate_main): def test_text_config_lamda_is_set(generate_main): """ - Test if lambda is set for lambda mode + Test if lambda is set for lambda mode (optimized with stateless lambda) """ # Given @@ -66,5 +66,5 @@ def test_text_config_lamda_is_set(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert "it_4->set_template([=]() -> esphome::optional {" in main_cpp + assert "it_4->set_template([]() -> esphome::optional {" in main_cpp assert 'return std::string{"Hello"};' in main_cpp diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 95633ca0c6..2c9f760c8e 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -173,6 +173,61 @@ class TestLambdaExpression: "}" ) + def test_str__stateless_no_return(self): + """Test stateless lambda (empty capture) generates correctly""" + target = cg.LambdaExpression( + ('ESP_LOGD("main", "Test message");',), + (), # No parameters + "", # Empty capture (stateless) + ) + + actual = str(target) + + assert actual == ('[]() {\n ESP_LOGD("main", "Test message");\n}') + + def test_str__stateless_with_return(self): + """Test stateless lambda with return type generates correctly""" + target = cg.LambdaExpression( + ("return global_value > 0;",), + (), # No parameters + "", # Empty capture (stateless) + bool, # Return type + ) + + actual = str(target) + + assert actual == ("[]() -> bool {\n return global_value > 0;\n}") + + def test_str__stateless_with_params(self): + """Test stateless lambda with parameters generates correctly""" + target = cg.LambdaExpression( + ("return foo + bar;",), + ((int, "foo"), (float, "bar")), + "", # Empty capture (stateless) + float, + ) + + actual = str(target) + + assert actual == ( + "[](int32_t foo, float bar) -> float {\n return foo + bar;\n}" + ) + + def test_str__with_capture(self): + """Test lambda with capture generates correctly""" + target = cg.LambdaExpression( + ("return captured_var + x;",), + ((int, "x"),), + "captured_var", # Has capture (not stateless) + int, + ) + + actual = str(target) + + assert actual == ( + "[captured_var](int32_t x) -> int32_t {\n return captured_var + x;\n}" + ) + class TestLiterals: @pytest.mark.parametrize( From b32ab802459126848d1a885b214de9fcfd95ab79 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Oct 2025 14:41:01 -0500 Subject: [PATCH 150/394] includes --- esphome/core/automation.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 5787373cbc..aace7889f0 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -4,6 +4,8 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" +#include +#include #include #include From 51e080c2d33e762ee13f417180a8039ceeee0ee5 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Mon, 27 Oct 2025 20:46:26 +0100 Subject: [PATCH 151/394] [substitutions] fix #11077 Preserve ESPHomeDatabase (document metadata) in substitutions (#11087) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- esphome/components/substitutions/__init__.py | 58 +++++++++++++-- esphome/components/substitutions/jinja.py | 78 ++++++++++++++++---- tests/unit_tests/test_substitutions.py | 27 +++++++ 3 files changed, 140 insertions(+), 23 deletions(-) diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index 098d56bfad..7e15f714f7 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -1,4 +1,6 @@ import logging +from re import Match +from typing import Any from esphome import core from esphome.config_helpers import Extend, Remove, merge_config, merge_dicts_ordered @@ -39,7 +41,34 @@ async def to_code(config): pass -def _expand_jinja(value, orig_value, path, jinja, ignore_missing): +def _restore_data_base(value: Any, orig_value: ESPHomeDataBase) -> ESPHomeDataBase: + """This function restores ESPHomeDataBase metadata held by the original string. + This is needed because during jinja evaluation, strings can be replaced by other types, + but we want to keep the original metadata for error reporting and source mapping. + For example, if a substitution replaces a string with a dictionary, we want that items + in the dictionary to still point to the original document location + """ + if isinstance(value, ESPHomeDataBase): + return value + if isinstance(value, dict): + return { + _restore_data_base(k, orig_value): _restore_data_base(v, orig_value) + for k, v in value.items() + } + if isinstance(value, list): + return [_restore_data_base(v, orig_value) for v in value] + if isinstance(value, str): + return make_data_base(value, orig_value) + return value + + +def _expand_jinja( + value: str | JinjaStr, + orig_value: str | JinjaStr, + path, + jinja: Jinja, + ignore_missing: bool, +) -> Any: if has_jinja(value): # If the original value passed in to this function is a JinjaStr, it means it contains an unresolved # Jinja expression from a previous pass. @@ -65,10 +94,17 @@ def _expand_jinja(value, orig_value, path, jinja, ignore_missing): f"\nSee {'->'.join(str(x) for x in path)}", path, ) + # If the original, unexpanded string, contained document metadata (ESPHomeDatabase), + # assign this same document metadata to the resulting value. + if isinstance(orig_value, ESPHomeDataBase): + value = _restore_data_base(value, orig_value) + return value -def _expand_substitutions(substitutions, value, path, jinja, ignore_missing): +def _expand_substitutions( + substitutions: dict, value: str, path, jinja: Jinja, ignore_missing: bool +) -> Any: if "$" not in value: return value @@ -76,14 +112,14 @@ def _expand_substitutions(substitutions, value, path, jinja, ignore_missing): i = 0 while True: - m = cv.VARIABLE_PROG.search(value, i) + m: Match[str] = cv.VARIABLE_PROG.search(value, i) if not m: # No more variable substitutions found. See if the remainder looks like a jinja template value = _expand_jinja(value, orig_value, path, jinja, ignore_missing) break i, j = m.span(0) - name = m.group(1) + name: str = m.group(1) if name.startswith("{") and name.endswith("}"): name = name[1:-1] if name not in substitutions: @@ -98,7 +134,7 @@ def _expand_substitutions(substitutions, value, path, jinja, ignore_missing): i = j continue - sub = substitutions[name] + sub: Any = substitutions[name] if i == 0 and j == len(value): # The variable spans the whole expression, e.g., "${varName}". Return its resolved value directly @@ -121,7 +157,13 @@ def _expand_substitutions(substitutions, value, path, jinja, ignore_missing): return value -def _substitute_item(substitutions, item, path, jinja, ignore_missing): +def _substitute_item( + substitutions: dict, + item: Any, + path: list[int | str], + jinja: Jinja, + ignore_missing: bool, +) -> Any | None: if isinstance(item, ESPLiteralValue): return None # do not substitute inside literal blocks if isinstance(item, list): @@ -160,7 +202,9 @@ def _substitute_item(substitutions, item, path, jinja, ignore_missing): return None -def do_substitution_pass(config, command_line_substitutions, ignore_missing=False): +def do_substitution_pass( + config: dict, command_line_substitutions: dict, ignore_missing: bool = False +) -> None: if CONF_SUBSTITUTIONS not in config and not command_line_substitutions: return diff --git a/esphome/components/substitutions/jinja.py b/esphome/components/substitutions/jinja.py index dde0162993..cb3c6dfac5 100644 --- a/esphome/components/substitutions/jinja.py +++ b/esphome/components/substitutions/jinja.py @@ -1,10 +1,14 @@ from ast import literal_eval +from collections.abc import Iterator +from itertools import chain, islice import logging import math import re +from types import GeneratorType +from typing import Any import jinja2 as jinja -from jinja2.sandbox import SandboxedEnvironment +from jinja2.nativetypes import NativeCodeGenerator, NativeTemplate from esphome.yaml_util import ESPLiteralValue @@ -24,7 +28,7 @@ detect_jinja_re = re.compile( ) -def has_jinja(st): +def has_jinja(st: str) -> bool: return detect_jinja_re.search(st) is not None @@ -109,12 +113,56 @@ class TrackerContext(jinja.runtime.Context): return val -class Jinja(SandboxedEnvironment): +def _concat_nodes_override(values: Iterator[Any]) -> Any: + """ + This function customizes how Jinja preserves native types when concatenating + multiple result nodes together. If the result is a single node, its value + is returned. Otherwise, the nodes are concatenated as strings. If + the result can be parsed with `ast.literal_eval`, the parsed + value is returned. Otherwise, the string is returned. + This helps preserve metadata such as ESPHomeDataBase from original values + and mimicks how HomeAssistant deals with template evaluation and preserving + the original datatype. + """ + head: list[Any] = list(islice(values, 2)) + + if not head: + return None + + if len(head) == 1: + raw = head[0] + if not isinstance(raw, str): + return raw + else: + if isinstance(values, GeneratorType): + values = chain(head, values) + raw = "".join([str(v) for v in values]) + + try: + # Attempt to parse the concatenated string into a Python literal. + # This allows expressions like "1 + 2" to be evaluated to the integer 3. + # If the result is also a string or there is a parsing error, + # fall back to returning the raw string. This is consistent with + # Home Assistant's behavior when evaluating templates + result = literal_eval(raw) + if not isinstance(result, str): + return result + + except (ValueError, SyntaxError, MemoryError, TypeError): + pass + return raw + + +class Jinja(jinja.Environment): """ Wraps a Jinja environment """ - def __init__(self, context_vars): + # jinja environment customization overrides + code_generator_class = NativeCodeGenerator + concat = staticmethod(_concat_nodes_override) + + def __init__(self, context_vars: dict): super().__init__( trim_blocks=True, lstrip_blocks=True, @@ -142,19 +190,10 @@ class Jinja(SandboxedEnvironment): **SAFE_GLOBALS, } - def safe_eval(self, expr): - try: - result = literal_eval(expr) - if not isinstance(result, str): - return result - except (ValueError, SyntaxError, MemoryError, TypeError): - pass - return expr - - def expand(self, content_str): + def expand(self, content_str: str | JinjaStr) -> Any: """ Renders a string that may contain Jinja expressions or statements - Returns the resulting processed string if all values could be resolved. + Returns the resulting value if all variables and expressions could be resolved. Otherwise, it returns a tagged (JinjaStr) string that captures variables in scope (upvalues), like a closure for later evaluation. """ @@ -172,7 +211,7 @@ class Jinja(SandboxedEnvironment): self.context_trace = {} try: template = self.from_string(content_str) - result = self.safe_eval(template.render(override_vars)) + result = template.render(override_vars) if isinstance(result, Undefined): print("" + result) # force a UndefinedError exception except (TemplateSyntaxError, UndefinedError) as err: @@ -201,3 +240,10 @@ class Jinja(SandboxedEnvironment): content_str.result = result return result, None + + +class JinjaTemplate(NativeTemplate): + environment_class = Jinja + + +Jinja.template_class = JinjaTemplate diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index beb1ebc73e..7d50b44506 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -1,6 +1,7 @@ import glob import logging from pathlib import Path +from typing import Any from esphome import config as config_module, yaml_util from esphome.components import substitutions @@ -60,6 +61,29 @@ def write_yaml(path: Path, data: dict) -> None: path.write_text(yaml_util.dump(data), encoding="utf-8") +def verify_database(value: Any, path: str = "") -> str | None: + if isinstance(value, list): + for i, v in enumerate(value): + result = verify_database(v, f"{path}[{i}]") + if result is not None: + return result + return None + if isinstance(value, dict): + for k, v in value.items(): + key_result = verify_database(k, f"{path}/{k}") + if key_result is not None: + return key_result + value_result = verify_database(v, f"{path}/{k}") + if value_result is not None: + return value_result + return None + if isinstance(value, str): + if not isinstance(value, yaml_util.ESPHomeDataBase): + return f"{path}: {value!r} is not ESPHomeDataBase" + return None + return None + + def test_substitutions_fixtures(fixture_path): base_dir = fixture_path / "substitutions" sources = sorted(glob.glob(str(base_dir / "*.input.yaml"))) @@ -83,6 +107,9 @@ def test_substitutions_fixtures(fixture_path): substitutions.do_substitution_pass(config, None) resolve_extend_remove(config) + verify_database_result = verify_database(config) + if verify_database_result is not None: + raise AssertionError(verify_database_result) # Also load expected using ESPHome's loader, or use {} if missing and DEV_MODE if expected_path.is_file(): From 00f22e5c3644f8c645d5741615271b78423ee453 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Oct 2025 14:51:08 -0500 Subject: [PATCH 152/394] [network] Eliminate runtime string parsing for IP address initialization (#11561) --- esphome/components/ethernet/__init__.py | 12 ++++---- esphome/components/network/__init__.py | 37 +++++++++++++++++++++++++ esphome/components/wifi/__init__.py | 6 ++-- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 77f70a3630..2f02d227d7 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -14,7 +14,7 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S2, VARIANT_ESP32S3, ) -from esphome.components.network import IPAddress +from esphome.components.network import ip_address_literal from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface import esphome.config_validation as cv from esphome.const import ( @@ -320,11 +320,11 @@ def _final_validate_spi(config): def manual_ip(config): return cg.StructInitializer( ManualIP, - ("static_ip", IPAddress(str(config[CONF_STATIC_IP]))), - ("gateway", IPAddress(str(config[CONF_GATEWAY]))), - ("subnet", IPAddress(str(config[CONF_SUBNET]))), - ("dns1", IPAddress(str(config[CONF_DNS1]))), - ("dns2", IPAddress(str(config[CONF_DNS2]))), + ("static_ip", ip_address_literal(config[CONF_STATIC_IP])), + ("gateway", ip_address_literal(config[CONF_GATEWAY])), + ("subnet", ip_address_literal(config[CONF_SUBNET])), + ("dns1", ip_address_literal(config[CONF_DNS1])), + ("dns2", ip_address_literal(config[CONF_DNS2])), ) diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 1a74350c4c..502803da1e 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -1,3 +1,5 @@ +import ipaddress + import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option import esphome.config_validation as cv @@ -10,6 +12,41 @@ AUTO_LOAD = ["mdns"] network_ns = cg.esphome_ns.namespace("network") IPAddress = network_ns.class_("IPAddress") + +def ip_address_literal(ip: str | int | None) -> cg.MockObj: + """Generate an IPAddress with compile-time initialization instead of runtime parsing. + + This function parses the IP address in Python during code generation and generates + a call to the 4-octet constructor (IPAddress(192, 168, 1, 1)) instead of the + string constructor (IPAddress("192.168.1.1")). This eliminates runtime string + parsing overhead and reduces flash usage on embedded systems. + + Args: + ip: IP address as string (e.g., "192.168.1.1"), ipaddress.IPv4Address, or None + + Returns: + IPAddress expression that uses 4-octet constructor for efficiency + """ + if ip is None: + return IPAddress(0, 0, 0, 0) + + try: + # Parse using Python's ipaddress module + ip_obj = ipaddress.ip_address(ip) + except (ValueError, TypeError): + pass + else: + # Only support IPv4 for now + if isinstance(ip_obj, ipaddress.IPv4Address): + # Extract octets from the packed bytes representation + octets = ip_obj.packed + # Generate call to 4-octet constructor: IPAddress(192, 168, 1, 1) + return IPAddress(octets[0], octets[1], octets[2], octets[3]) + + # Fallback to string constructor if parsing fails + return IPAddress(str(ip)) + + CONFIG_SCHEMA = cv.Schema( { cv.SplitDefault( diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index ba488728b7..b980bab4aa 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -3,7 +3,7 @@ from esphome.automation import Condition import esphome.codegen as cg from esphome.components.const import CONF_USE_PSRAM from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant -from esphome.components.network import IPAddress +from esphome.components.network import ip_address_literal 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 @@ -334,9 +334,7 @@ def eap_auth(config): def safe_ip(ip): - if ip is None: - return IPAddress(0, 0, 0, 0) - return IPAddress(str(ip)) + return ip_address_literal(ip) def manual_ip(config): From e26b5874d76332aa35125e3be434a9d7ab525d3a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Oct 2025 15:07:31 -0500 Subject: [PATCH 153/394] [api] Register user services with initializer_list (#11545) --- esphome/components/api/__init__.py | 10 +++++++++- esphome/components/api/api_server.h | 6 ++++++ esphome/components/api/custom_api_device.h | 12 ++++++++++++ esphome/core/defines.h | 1 + 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index e91e922204..363f5b73e1 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -258,6 +258,10 @@ async def to_code(config): if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]: cg.add_define("USE_API_SERVICES") + # Set USE_API_CUSTOM_SERVICES if external components need dynamic service registration + if config[CONF_CUSTOM_SERVICES]: + cg.add_define("USE_API_CUSTOM_SERVICES") + if config[CONF_HOMEASSISTANT_SERVICES]: cg.add_define("USE_API_HOMEASSISTANT_SERVICES") @@ -265,6 +269,8 @@ async def to_code(config): cg.add_define("USE_API_HOMEASSISTANT_STATES") if actions := config.get(CONF_ACTIONS, []): + # Collect all triggers first, then register all at once with initializer_list + triggers: list[cg.Pvariable] = [] for conf in actions: template_args = [] func_args = [] @@ -278,8 +284,10 @@ async def to_code(config): trigger = cg.new_Pvariable( conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names ) - cg.add(var.register_user_service(trigger)) + triggers.append(trigger) await automation.build_automation(trigger, func_args, conf) + # Register all services at once - single allocation, no reallocations + cg.add(var.initialize_user_services(triggers)) if CONF_ON_CLIENT_CONNECTED in config: cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER") diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index e0e23301d0..d29181250e 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -125,8 +125,14 @@ class APIServer : public Component, public Controller { #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif // USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_SERVICES + void initialize_user_services(std::initializer_list services) { + this->user_services_.assign(services); + } +#ifdef USE_API_CUSTOM_SERVICES + // Only compile push_back method when custom_services: true (external components) void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } #endif +#endif #ifdef USE_HOMEASSISTANT_TIME void request_time(); #endif diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h index 711eba2444..d34ccfa0ce 100644 --- a/esphome/components/api/custom_api_device.h +++ b/esphome/components/api/custom_api_device.h @@ -53,8 +53,14 @@ class CustomAPIDevice { template void register_service(void (T::*callback)(Ts...), const std::string &name, const std::array &arg_names) { +#ifdef USE_API_CUSTOM_SERVICES auto *service = new CustomAPIDeviceService(name, arg_names, (T *) this, callback); // NOLINT global_api_server->register_user_service(service); +#else + static_assert( + sizeof(T) == 0, + "register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration"); +#endif } #else template @@ -86,8 +92,14 @@ class CustomAPIDevice { */ #ifdef USE_API_SERVICES 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 global_api_server->register_user_service(service); +#else + static_assert( + sizeof(T) == 0, + "register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration"); +#endif } #else template void register_service(void (T::*callback)(), const std::string &name) { diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 8095ffed4a..97e766455a 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -123,6 +123,7 @@ #define USE_API_NOISE #define USE_API_PLAINTEXT #define USE_API_SERVICES +#define USE_API_CUSTOM_SERVICES #define API_MAX_SEND_QUEUE 8 #define USE_MD5 #define USE_SHA256 From 14b057f54e5f552de9ef1450cea509616bb1b230 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Oct 2025 15:14:16 -0500 Subject: [PATCH 154/394] [light] Optimize LambdaLightEffect and AddressableLambdaLightEffect with function pointers (#11556) --- esphome/components/light/addressable_light_effect.h | 6 +++--- esphome/components/light/base_light_effects.h | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 9840112040..0847db3770 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -57,9 +57,9 @@ class AddressableLightEffect : public LightEffect { class AddressableLambdaLightEffect : public AddressableLightEffect { public: - AddressableLambdaLightEffect(const char *name, std::function f, + AddressableLambdaLightEffect(const char *name, void (*f)(AddressableLight &, Color, bool initial_run), uint32_t update_interval) - : AddressableLightEffect(name), f_(std::move(f)), update_interval_(update_interval) {} + : AddressableLightEffect(name), f_(f), update_interval_(update_interval) {} void start() override { this->initial_run_ = true; } void apply(AddressableLight &it, const Color ¤t_color) override { const uint32_t now = millis(); @@ -72,7 +72,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect { } protected: - std::function f_; + void (*f_)(AddressableLight &, Color, bool initial_run); uint32_t update_interval_; uint32_t last_run_{0}; bool initial_run_; diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index 327c243525..515afc5c59 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -112,8 +112,8 @@ class RandomLightEffect : public LightEffect { class LambdaLightEffect : public LightEffect { public: - LambdaLightEffect(const char *name, std::function f, uint32_t update_interval) - : LightEffect(name), f_(std::move(f)), update_interval_(update_interval) {} + LambdaLightEffect(const char *name, void (*f)(bool initial_run), uint32_t update_interval) + : LightEffect(name), f_(f), update_interval_(update_interval) {} void start() override { this->initial_run_ = true; } void apply() override { @@ -130,7 +130,7 @@ class LambdaLightEffect : public LightEffect { uint32_t get_current_index() const { return this->get_index(); } protected: - std::function f_; + void (*f_)(bool initial_run); uint32_t update_interval_; uint32_t last_run_{0}; bool initial_run_; From bda4769bd3df0ac81eae0c4dcdcc5dc49e960eb7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Oct 2025 16:05:40 -0500 Subject: [PATCH 155/394] [core] Optimize TemplatableValue to use function pointers for stateless lambdas (#11554) --- esphome/core/automation.h | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 0512752d50..aace7889f0 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -4,6 +4,8 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" +#include +#include #include #include @@ -27,11 +29,20 @@ template class TemplatableValue { public: TemplatableValue() : type_(NONE) {} - template::value, int> = 0> TemplatableValue(F value) : type_(VALUE) { + template TemplatableValue(F value) requires(!std::invocable) : type_(VALUE) { new (&this->value_) T(std::move(value)); } - template::value, int> = 0> TemplatableValue(F f) : type_(LAMBDA) { + // For stateless lambdas (convertible to function pointer): use function pointer + template + TemplatableValue(F f) requires std::invocable && std::convertible_to + : type_(STATELESS_LAMBDA) { + this->stateless_f_ = f; // Implicit conversion to function pointer + } + + // For stateful lambdas (not convertible to function pointer): use std::function + template + TemplatableValue(F f) requires std::invocable &&(!std::convertible_to) : type_(LAMBDA) { this->f_ = new std::function(std::move(f)); } @@ -41,6 +52,8 @@ template class TemplatableValue { new (&this->value_) T(other.value_); } else if (type_ == LAMBDA) { this->f_ = new std::function(*other.f_); + } else if (type_ == STATELESS_LAMBDA) { + this->stateless_f_ = other.stateless_f_; } } @@ -51,6 +64,8 @@ template class TemplatableValue { } else if (type_ == LAMBDA) { this->f_ = other.f_; other.f_ = nullptr; + } else if (type_ == STATELESS_LAMBDA) { + this->stateless_f_ = other.stateless_f_; } other.type_ = NONE; } @@ -78,16 +93,23 @@ template class TemplatableValue { } else if (type_ == LAMBDA) { delete this->f_; } + // STATELESS_LAMBDA/NONE: no cleanup needed (function pointer or empty, not heap-allocated) } bool has_value() { return this->type_ != NONE; } T value(X... x) { - if (this->type_ == LAMBDA) { - return (*this->f_)(x...); + switch (this->type_) { + case STATELESS_LAMBDA: + return this->stateless_f_(x...); // Direct function pointer call + case LAMBDA: + return (*this->f_)(x...); // std::function call + case VALUE: + return this->value_; + case NONE: + default: + return T{}; } - // return value also when none - return this->type_ == VALUE ? this->value_ : T{}; } optional optional_value(X... x) { @@ -109,11 +131,13 @@ template class TemplatableValue { NONE, VALUE, LAMBDA, + STATELESS_LAMBDA, } type_; union { T value_; std::function *f_; + T (*stateless_f_)(X...); }; }; From f44615cc8daac4ee1b9e7830c2adf8cca91f1667 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Oct 2025 17:00:02 -0500 Subject: [PATCH 156/394] [template] Optimize all template platforms to use function pointers for stateless lambdas (#11555) --- esphome/components/template/binary_sensor/__init__.py | 8 +++++++- .../template/binary_sensor/template_binary_sensor.cpp | 4 ++-- .../template/binary_sensor/template_binary_sensor.h | 4 ++-- esphome/components/template/cover/template_cover.cpp | 4 ++-- esphome/components/template/cover/template_cover.h | 8 ++++---- esphome/components/template/datetime/template_date.h | 4 ++-- esphome/components/template/datetime/template_datetime.h | 4 ++-- esphome/components/template/datetime/template_time.h | 4 ++-- esphome/components/template/lock/template_lock.cpp | 2 +- esphome/components/template/lock/template_lock.h | 4 ++-- esphome/components/template/number/template_number.h | 4 ++-- esphome/components/template/select/template_select.h | 4 ++-- esphome/components/template/sensor/template_sensor.cpp | 2 +- esphome/components/template/sensor/template_sensor.h | 4 ++-- esphome/components/template/switch/template_switch.cpp | 2 +- esphome/components/template/switch/template_switch.h | 4 ++-- esphome/components/template/text/template_text.h | 4 ++-- .../template/text_sensor/template_text_sensor.cpp | 2 +- .../template/text_sensor/template_text_sensor.h | 4 ++-- esphome/components/template/valve/template_valve.cpp | 2 +- esphome/components/template/valve/template_valve.h | 4 ++-- 21 files changed, 44 insertions(+), 38 deletions(-) diff --git a/esphome/components/template/binary_sensor/__init__.py b/esphome/components/template/binary_sensor/__init__.py index c93876380d..9d4208dcca 100644 --- a/esphome/components/template/binary_sensor/__init__.py +++ b/esphome/components/template/binary_sensor/__init__.py @@ -38,8 +38,14 @@ async def to_code(config): condition = await automation.build_condition( condition, cg.TemplateArguments(), [] ) + # Generate a stateless lambda that calls condition.check() + # capture="" is safe because condition is a global variable in generated C++ code + # and doesn't need to be captured. This allows implicit conversion to function pointer. template_ = LambdaExpression( - f"return {condition.check()};", [], return_type=cg.optional.template(bool) + f"return {condition.check()};", + [], + return_type=cg.optional.template(bool), + capture="", ) cg.add(var.set_template(template_)) diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.cpp b/esphome/components/template/binary_sensor/template_binary_sensor.cpp index d1fb618695..8543dff4dc 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.cpp +++ b/esphome/components/template/binary_sensor/template_binary_sensor.cpp @@ -9,10 +9,10 @@ static const char *const TAG = "template.binary_sensor"; void TemplateBinarySensor::setup() { this->loop(); } void TemplateBinarySensor::loop() { - if (this->f_ == nullptr) + if (!this->f_.has_value()) return; - auto s = this->f_(); + auto s = (*this->f_)(); if (s.has_value()) { this->publish_state(*s); } diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.h b/esphome/components/template/binary_sensor/template_binary_sensor.h index 5e5624d82e..2e0b216eb4 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.h +++ b/esphome/components/template/binary_sensor/template_binary_sensor.h @@ -8,7 +8,7 @@ namespace template_ { class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void loop() override; @@ -17,7 +17,7 @@ class TemplateBinarySensor : public Component, public binary_sensor::BinarySenso float get_setup_priority() const override { return setup_priority::HARDWARE; } protected: - std::function()> f_{nullptr}; + optional (*)()> f_; }; } // namespace template_ diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index 84c687536e..bed3931e78 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -63,7 +63,7 @@ void TemplateCover::loop() { } void TemplateCover::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void TemplateCover::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } -void TemplateCover::set_state_lambda(std::function()> &&f) { this->state_f_ = f; } +void TemplateCover::set_state_lambda(optional (*f)()) { this->state_f_ = f; } float TemplateCover::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; } Trigger<> *TemplateCover::get_close_trigger() const { return this->close_trigger_; } @@ -124,7 +124,7 @@ CoverTraits TemplateCover::get_traits() { } Trigger *TemplateCover::get_position_trigger() const { return this->position_trigger_; } Trigger *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; } -void TemplateCover::set_tilt_lambda(std::function()> &&tilt_f) { this->tilt_f_ = tilt_f; } +void TemplateCover::set_tilt_lambda(optional (*tilt_f)()) { this->tilt_f_ = tilt_f; } void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } void TemplateCover::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index 958c94b0a6..ed1ebf4e43 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -17,7 +17,7 @@ class TemplateCover : public cover::Cover, public Component { public: TemplateCover(); - void set_state_lambda(std::function()> &&f); + void set_state_lambda(optional (*f)()); Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; @@ -26,7 +26,7 @@ class TemplateCover : public cover::Cover, public Component { Trigger *get_tilt_trigger() const; void set_optimistic(bool optimistic); void set_assumed_state(bool assumed_state); - void set_tilt_lambda(std::function()> &&tilt_f); + void set_tilt_lambda(optional (*tilt_f)()); void set_has_stop(bool has_stop); void set_has_position(bool has_position); void set_has_tilt(bool has_tilt); @@ -45,8 +45,8 @@ class TemplateCover : public cover::Cover, public Component { void stop_prev_trigger_(); TemplateCoverRestoreMode restore_mode_{COVER_RESTORE}; - optional()>> state_f_; - optional()>> tilt_f_; + optional (*)()> state_f_; + optional (*)()> tilt_f_; bool assumed_state_{false}; bool optimistic_{false}; Trigger<> *open_trigger_; diff --git a/esphome/components/template/datetime/template_date.h b/esphome/components/template/datetime/template_date.h index 185c7ed49d..2a0967fc94 100644 --- a/esphome/components/template/datetime/template_date.h +++ b/esphome/components/template/datetime/template_date.h @@ -15,7 +15,7 @@ namespace template_ { class TemplateDate : public datetime::DateEntity, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void update() override; @@ -35,7 +35,7 @@ class TemplateDate : public datetime::DateEntity, public PollingComponent { ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + optional (*)()> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/datetime/template_datetime.h b/esphome/components/template/datetime/template_datetime.h index ef80ded89a..d917015b67 100644 --- a/esphome/components/template/datetime/template_datetime.h +++ b/esphome/components/template/datetime/template_datetime.h @@ -15,7 +15,7 @@ namespace template_ { class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void update() override; @@ -35,7 +35,7 @@ class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponen ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + optional (*)()> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/datetime/template_time.h b/esphome/components/template/datetime/template_time.h index 4a7c0098ec..2f05ba0737 100644 --- a/esphome/components/template/datetime/template_time.h +++ b/esphome/components/template/datetime/template_time.h @@ -15,7 +15,7 @@ namespace template_ { class TemplateTime : public datetime::TimeEntity, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void update() override; @@ -35,7 +35,7 @@ class TemplateTime : public datetime::TimeEntity, public PollingComponent { ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + optional (*)()> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/lock/template_lock.cpp b/esphome/components/template/lock/template_lock.cpp index 87ba1046eb..c2e227c26d 100644 --- a/esphome/components/template/lock/template_lock.cpp +++ b/esphome/components/template/lock/template_lock.cpp @@ -45,7 +45,7 @@ void TemplateLock::open_latch() { this->open_trigger_->trigger(); } void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } -void TemplateLock::set_state_lambda(std::function()> &&f) { this->f_ = f; } +void TemplateLock::set_state_lambda(optional (*f)()) { this->f_ = f; } float TemplateLock::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateLock::get_lock_trigger() const { return this->lock_trigger_; } Trigger<> *TemplateLock::get_unlock_trigger() const { return this->unlock_trigger_; } diff --git a/esphome/components/template/lock/template_lock.h b/esphome/components/template/lock/template_lock.h index 4f798eca81..428744a66f 100644 --- a/esphome/components/template/lock/template_lock.h +++ b/esphome/components/template/lock/template_lock.h @@ -13,7 +13,7 @@ class TemplateLock : public lock::Lock, public Component { void dump_config() override; - void set_state_lambda(std::function()> &&f); + void set_state_lambda(optional (*f)()); Trigger<> *get_lock_trigger() const; Trigger<> *get_unlock_trigger() const; Trigger<> *get_open_trigger() const; @@ -26,7 +26,7 @@ class TemplateLock : public lock::Lock, public Component { void control(const lock::LockCall &call) override; void open_latch() override; - optional()>> f_; + optional (*)()> f_; bool optimistic_{false}; Trigger<> *lock_trigger_; Trigger<> *unlock_trigger_; diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h index 9a82e44339..e77b181d25 100644 --- a/esphome/components/template/number/template_number.h +++ b/esphome/components/template/number/template_number.h @@ -10,7 +10,7 @@ namespace template_ { class TemplateNumber : public number::Number, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void update() override; @@ -28,7 +28,7 @@ class TemplateNumber : public number::Number, public PollingComponent { float initial_value_{NAN}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + optional (*)()> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 2f00765c3d..c1b348b26a 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -10,7 +10,7 @@ namespace template_ { class TemplateSelect : public select::Select, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void update() override; @@ -28,7 +28,7 @@ class TemplateSelect : public select::Select, public PollingComponent { std::string initial_option_; bool restore_value_ = false; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + optional (*)()> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/sensor/template_sensor.cpp b/esphome/components/template/sensor/template_sensor.cpp index f2d0e7363e..65f2417670 100644 --- a/esphome/components/template/sensor/template_sensor.cpp +++ b/esphome/components/template/sensor/template_sensor.cpp @@ -17,7 +17,7 @@ void TemplateSensor::update() { } } float TemplateSensor::get_setup_priority() const { return setup_priority::HARDWARE; } -void TemplateSensor::set_template(std::function()> &&f) { this->f_ = f; } +void TemplateSensor::set_template(optional (*f)()) { this->f_ = f; } void TemplateSensor::dump_config() { LOG_SENSOR("", "Template Sensor", this); LOG_UPDATE_INTERVAL(this); diff --git a/esphome/components/template/sensor/template_sensor.h b/esphome/components/template/sensor/template_sensor.h index 2630cb0b14..369313d607 100644 --- a/esphome/components/template/sensor/template_sensor.h +++ b/esphome/components/template/sensor/template_sensor.h @@ -8,7 +8,7 @@ namespace template_ { class TemplateSensor : public sensor::Sensor, public PollingComponent { public: - void set_template(std::function()> &&f); + void set_template(optional (*f)()); void update() override; @@ -17,7 +17,7 @@ class TemplateSensor : public sensor::Sensor, public PollingComponent { float get_setup_priority() const override; protected: - optional()>> f_; + optional (*)()> f_; }; } // namespace template_ diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index fa236f6364..5aaf514b2a 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -35,7 +35,7 @@ void TemplateSwitch::write_state(bool state) { } void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } bool TemplateSwitch::assumed_state() { return this->assumed_state_; } -void TemplateSwitch::set_state_lambda(std::function()> &&f) { this->f_ = f; } +void TemplateSwitch::set_state_lambda(optional (*f)()) { this->f_ = f; } float TemplateSwitch::get_setup_priority() const { return setup_priority::HARDWARE - 2.0f; } Trigger<> *TemplateSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } Trigger<> *TemplateSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index bfe9ac25d6..0fba66b9bd 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -14,7 +14,7 @@ class TemplateSwitch : public switch_::Switch, public Component { void setup() override; void dump_config() override; - void set_state_lambda(std::function()> &&f); + void set_state_lambda(optional (*f)()); Trigger<> *get_turn_on_trigger() const; Trigger<> *get_turn_off_trigger() const; void set_optimistic(bool optimistic); @@ -28,7 +28,7 @@ class TemplateSwitch : public switch_::Switch, public Component { void write_state(bool state) override; - optional()>> f_; + optional (*)()> f_; bool optimistic_{false}; bool assumed_state_{false}; Trigger<> *turn_on_trigger_; diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index bcfc54a2ba..6c17d2016a 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -61,7 +61,7 @@ template class TextSaver : public TemplateTextSaverBase { class TemplateText : public text::Text, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void update() override; @@ -78,7 +78,7 @@ class TemplateText : public text::Text, public PollingComponent { bool optimistic_ = false; std::string initial_value_; Trigger *set_trigger_ = new Trigger(); - optional()>> f_{nullptr}; + optional (*)()> f_{nullptr}; TemplateTextSaverBase *pref_ = nullptr; }; diff --git a/esphome/components/template/text_sensor/template_text_sensor.cpp b/esphome/components/template/text_sensor/template_text_sensor.cpp index 885ad47bbf..2b0297d62f 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.cpp +++ b/esphome/components/template/text_sensor/template_text_sensor.cpp @@ -16,7 +16,7 @@ void TemplateTextSensor::update() { } } float TemplateTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } -void TemplateTextSensor::set_template(std::function()> &&f) { this->f_ = f; } +void TemplateTextSensor::set_template(optional (*f)()) { this->f_ = f; } void TemplateTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Template Sensor", this); } } // namespace template_ diff --git a/esphome/components/template/text_sensor/template_text_sensor.h b/esphome/components/template/text_sensor/template_text_sensor.h index 07a2bd96fc..48e40c2493 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.h +++ b/esphome/components/template/text_sensor/template_text_sensor.h @@ -9,7 +9,7 @@ namespace template_ { class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent { public: - void set_template(std::function()> &&f); + void set_template(optional (*f)()); void update() override; @@ -18,7 +18,7 @@ class TemplateTextSensor : public text_sensor::TextSensor, public PollingCompone void dump_config() override; protected: - optional()>> f_{}; + optional (*)()> f_{}; }; } // namespace template_ diff --git a/esphome/components/template/valve/template_valve.cpp b/esphome/components/template/valve/template_valve.cpp index 5fa14a2de7..b27cc00968 100644 --- a/esphome/components/template/valve/template_valve.cpp +++ b/esphome/components/template/valve/template_valve.cpp @@ -55,7 +55,7 @@ void TemplateValve::loop() { void TemplateValve::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void TemplateValve::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } -void TemplateValve::set_state_lambda(std::function()> &&f) { this->state_f_ = f; } +void TemplateValve::set_state_lambda(optional (*f)()) { this->state_f_ = f; } float TemplateValve::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateValve::get_open_trigger() const { return this->open_trigger_; } diff --git a/esphome/components/template/valve/template_valve.h b/esphome/components/template/valve/template_valve.h index 5e3fb6aff3..92c32f3487 100644 --- a/esphome/components/template/valve/template_valve.h +++ b/esphome/components/template/valve/template_valve.h @@ -17,7 +17,7 @@ class TemplateValve : public valve::Valve, public Component { public: TemplateValve(); - void set_state_lambda(std::function()> &&f); + void set_state_lambda(optional (*f)()); Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; @@ -42,7 +42,7 @@ class TemplateValve : public valve::Valve, public Component { void stop_prev_trigger_(); TemplateValveRestoreMode restore_mode_{VALVE_NO_RESTORE}; - optional()>> state_f_; + optional (*)()> state_f_; bool assumed_state_{false}; bool optimistic_{false}; Trigger<> *open_trigger_; From dfa69173ead5457f23aa6daf49bf85aa3dd59e59 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Oct 2025 17:03:44 -0500 Subject: [PATCH 157/394] [api] Use FixedVector const references for service array arguments (#11546) --- esphome/codegen.py | 1 + esphome/components/api/__init__.py | 10 +++-- esphome/components/api/user_services.cpp | 57 ++++++++++++++++++++++-- esphome/cpp_types.py | 1 + 4 files changed, 61 insertions(+), 8 deletions(-) diff --git a/esphome/codegen.py b/esphome/codegen.py index 6decd77c62..6d55c6023d 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -62,6 +62,7 @@ from esphome.cpp_types import ( # noqa: F401 EntityBase, EntityCategory, ESPTime, + FixedVector, GPIOPin, InternalGPIOPin, JsonObject, diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 363f5b73e1..449572c0e5 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -71,10 +71,12 @@ SERVICE_ARG_NATIVE_TYPES = { "int": cg.int32, "float": float, "string": cg.std_string, - "bool[]": cg.std_vector.template(bool), - "int[]": cg.std_vector.template(cg.int32), - "float[]": cg.std_vector.template(float), - "string[]": cg.std_vector.template(cg.std_string), + "bool[]": cg.FixedVector.template(bool).operator("const").operator("ref"), + "int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"), + "float[]": cg.FixedVector.template(float).operator("const").operator("ref"), + "string[]": cg.FixedVector.template(cg.std_string) + .operator("const") + .operator("ref"), } CONF_ENCRYPTION = "encryption" CONF_BATCH_DELAY = "batch_delay" diff --git a/esphome/components/api/user_services.cpp b/esphome/components/api/user_services.cpp index 3cbf2ab5f9..9c2b4aa79a 100644 --- a/esphome/components/api/user_services.cpp +++ b/esphome/components/api/user_services.cpp @@ -11,23 +11,58 @@ template<> int32_t get_execute_arg_value(const ExecuteServiceArgument & } template<> float get_execute_arg_value(const ExecuteServiceArgument &arg) { return arg.float_; } template<> std::string get_execute_arg_value(const ExecuteServiceArgument &arg) { return arg.string_; } + +// Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { - return std::vector(arg.bool_array.begin(), arg.bool_array.end()); + std::vector result; + result.reserve(arg.bool_array.size()); + result.insert(result.end(), arg.bool_array.begin(), arg.bool_array.end()); + return result; } template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { - return std::vector(arg.int_array.begin(), arg.int_array.end()); + std::vector result; + result.reserve(arg.int_array.size()); + result.insert(result.end(), arg.int_array.begin(), arg.int_array.end()); + return result; } template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { - return std::vector(arg.float_array.begin(), arg.float_array.end()); + std::vector result; + result.reserve(arg.float_array.size()); + result.insert(result.end(), arg.float_array.begin(), arg.float_array.end()); + return result; } template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { - return std::vector(arg.string_array.begin(), arg.string_array.end()); + std::vector result; + result.reserve(arg.string_array.size()); + result.insert(result.end(), arg.string_array.begin(), arg.string_array.end()); + return result; +} + +// New FixedVector const reference versions for YAML-generated services - zero-copy +template<> +const FixedVector &get_execute_arg_value &>(const ExecuteServiceArgument &arg) { + return arg.bool_array; +} +template<> +const FixedVector &get_execute_arg_value &>(const ExecuteServiceArgument &arg) { + return arg.int_array; +} +template<> +const FixedVector &get_execute_arg_value &>(const ExecuteServiceArgument &arg) { + return arg.float_array; +} +template<> +const FixedVector &get_execute_arg_value &>( + const ExecuteServiceArgument &arg) { + return arg.string_array; } template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_BOOL; } template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_INT; } template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_FLOAT; } template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_STRING; } + +// Legacy std::vector versions for external components using custom_api_device.h template<> enums::ServiceArgType to_service_arg_type>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; } template<> enums::ServiceArgType to_service_arg_type>() { return enums::SERVICE_ARG_TYPE_INT_ARRAY; @@ -39,4 +74,18 @@ template<> enums::ServiceArgType to_service_arg_type>() return enums::SERVICE_ARG_TYPE_STRING_ARRAY; } +// New FixedVector const reference versions for YAML-generated services +template<> enums::ServiceArgType to_service_arg_type &>() { + return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; +} +template<> enums::ServiceArgType to_service_arg_type &>() { + return enums::SERVICE_ARG_TYPE_INT_ARRAY; +} +template<> enums::ServiceArgType to_service_arg_type &>() { + return enums::SERVICE_ARG_TYPE_FLOAT_ARRAY; +} +template<> enums::ServiceArgType to_service_arg_type &>() { + return enums::SERVICE_ARG_TYPE_STRING_ARRAY; +} + } // namespace esphome::api diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index a0dd62cb4e..0d1813f63b 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -23,6 +23,7 @@ size_t = global_ns.namespace("size_t") const_char_ptr = global_ns.namespace("const char *") NAN = global_ns.namespace("NAN") esphome_ns = global_ns # using namespace esphome; +FixedVector = esphome_ns.class_("FixedVector") App = esphome_ns.App EntityBase = esphome_ns.class_("EntityBase") Component = esphome_ns.class_("Component") From d65ad693381b3f2e61e32093577f3322fd9fa5b8 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 27 Oct 2025 17:09:45 -0500 Subject: [PATCH 158/394] [uart] Fix order of initialization calls (#11510) --- .../uart/uart_component_esp_idf.cpp | 66 ++++++++++--------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index cffa3308eb..73813d2d5b 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -99,10 +99,26 @@ void IDFUARTComponent::setup() { } void IDFUARTComponent::load_settings(bool dump_config) { - uart_config_t uart_config = this->get_config_(); - esp_err_t err = uart_param_config(this->uart_num_, &uart_config); + esp_err_t err; + + if (uart_is_driver_installed(this->uart_num_)) { + err = uart_driver_delete(this->uart_num_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + } + err = uart_driver_install(this->uart_num_, // UART number + this->rx_buffer_size_, // RX ring buffer size + 0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will + // block task until all data has been sent out + 20, // event queue size/depth + &this->uart_event_queue_, // event queue + 0 // Flags used to allocate the interrupt + ); if (err != ESP_OK) { - ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); + ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); this->mark_failed(); return; } @@ -119,10 +135,12 @@ void IDFUARTComponent::load_settings(bool dump_config) { int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; uint32_t invert = 0; - if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) + if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) { invert |= UART_SIGNAL_TXD_INV; - if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) + } + if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) { invert |= UART_SIGNAL_RXD_INV; + } err = uart_set_line_inverse(this->uart_num_, invert); if (err != ESP_OK) { @@ -138,26 +156,6 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } - if (uart_is_driver_installed(this->uart_num_)) { - uart_driver_delete(this->uart_num_); - if (err != ESP_OK) { - ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } - } - err = uart_driver_install(this->uart_num_, /* UART RX ring buffer size. */ this->rx_buffer_size_, - /* UART TX ring buffer size. If set to zero, driver will not use TX buffer, TX function will - block task until all data have been sent out.*/ - 0, - /* UART event queue size/depth. */ 20, &(this->uart_event_queue_), - /* Flags used to allocate the interrupt. */ 0); - if (err != ESP_OK) { - ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } - err = uart_set_rx_full_threshold(this->uart_num_, this->rx_full_threshold_); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_set_rx_full_threshold failed: %s", esp_err_to_name(err)); @@ -173,24 +171,32 @@ void IDFUARTComponent::load_settings(bool dump_config) { } auto mode = this->flow_control_pin_ != nullptr ? UART_MODE_RS485_HALF_DUPLEX : UART_MODE_UART; - err = uart_set_mode(this->uart_num_, mode); + err = uart_set_mode(this->uart_num_, mode); // per docs, must be called only after uart_driver_install() if (err != ESP_OK) { ESP_LOGW(TAG, "uart_set_mode failed: %s", esp_err_to_name(err)); this->mark_failed(); return; } + uart_config_t uart_config = this->get_config_(); + err = uart_param_config(this->uart_num_, &uart_config); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + if (dump_config) { - ESP_LOGCONFIG(TAG, "UART %u was reloaded.", this->uart_num_); + ESP_LOGCONFIG(TAG, "Reloaded UART %u", this->uart_num_); this->dump_config(); } } void IDFUARTComponent::dump_config() { ESP_LOGCONFIG(TAG, "UART Bus %u:", this->uart_num_); - LOG_PIN(" TX Pin: ", tx_pin_); - LOG_PIN(" RX Pin: ", rx_pin_); - LOG_PIN(" Flow Control Pin: ", flow_control_pin_); + LOG_PIN(" TX Pin: ", this->tx_pin_); + LOG_PIN(" RX Pin: ", this->rx_pin_); + LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_); if (this->rx_pin_ != nullptr) { ESP_LOGCONFIG(TAG, " RX Buffer Size: %u\n" From 3377080272ea4b6a201cda35e1d97e504d16b247 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Oct 2025 17:16:09 -0500 Subject: [PATCH 159/394] [core] Simplify ESPTime::strftime() and save 20 bytes flash (#11539) --- esphome/core/time.cpp | 22 ++++++++-------------- esphome/core/time.h | 12 +++++++----- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index 1285ec6448..d30dac4394 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -46,24 +46,18 @@ struct tm ESPTime::to_c_tm() { return c_tm; } -std::string ESPTime::strftime(const std::string &format) { - std::string timestr; - timestr.resize(format.size() * 4); +std::string ESPTime::strftime(const char *format) { struct tm c_tm = this->to_c_tm(); - size_t len = ::strftime(×tr[0], timestr.size(), format.c_str(), &c_tm); - while (len == 0) { - if (timestr.size() >= 128) { - // strftime has failed for reasons unrelated to the size of the buffer - // so return a formatting error - return "ERROR"; - } - timestr.resize(timestr.size() * 2); - len = ::strftime(×tr[0], timestr.size(), format.c_str(), &c_tm); + char buf[128]; + size_t len = ::strftime(buf, sizeof(buf), format, &c_tm); + if (len > 0) { + return std::string(buf, len); } - timestr.resize(len); - return timestr; + return "ERROR"; } +std::string ESPTime::strftime(const std::string &format) { return this->strftime(format.c_str()); } + bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) { uint16_t year; uint8_t month; diff --git a/esphome/core/time.h b/esphome/core/time.h index a53fca2346..ffcfced418 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -44,17 +44,19 @@ struct ESPTime { size_t strftime(char *buffer, size_t buffer_len, const char *format); /** Convert this ESPTime struct to a string as specified by the format argument. - * @see https://www.gnu.org/software/libc/manual/html_node/Formatting-Calendar-Time.html#index-strftime + * @see https://en.cppreference.com/w/c/chrono/strftime * - * @warning This method uses dynamically allocated strings which can cause heap fragmentation with some + * @warning This method returns a dynamically allocated string which can cause heap fragmentation with some * microcontrollers. * - * @warning This method can return "ERROR" when the underlying strftime() call fails, e.g. when the - * format string contains unsupported specifiers or when the format string doesn't produce any - * output. + * @warning This method can return "ERROR" when the underlying strftime() call fails or when the + * output exceeds 128 bytes. */ std::string strftime(const std::string &format); + /// @copydoc strftime(const std::string &format) + std::string strftime(const char *format); + /// Check if this ESPTime is valid (all fields in range and year is greater than 2018) bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); } From 31b1b50ad9d6811b1077fdedc6ae2df850c30fa0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Oct 2025 17:16:38 -0500 Subject: [PATCH 160/394] [number] Skip set_mode call when using default AUTO mode (#11537) --- esphome/components/number/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 230c3aa0c1..ac0329fcc6 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -252,7 +252,10 @@ async def setup_number_core_( cg.add(var.traits.set_max_value(max_value)) cg.add(var.traits.set_step(step)) - cg.add(var.traits.set_mode(config[CONF_MODE])) + # Only set if non-default to avoid bloating setup() function + # (mode_ is initialized to NUMBER_MODE_AUTO in the header) + if config[CONF_MODE] != NumberMode.NUMBER_MODE_AUTO: + cg.add(var.traits.set_mode(config[CONF_MODE])) for conf in config.get(CONF_ON_VALUE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) From 733001bf651aaa01ca931bbc414e849588988104 Mon Sep 17 00:00:00 2001 From: clydebarrow <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 28 Oct 2025 08:32:24 +1000 Subject: [PATCH 161/394] Fix warning about shift overflow --- esphome/components/usb_host/usb_host.h | 1 + esphome/components/usb_host/usb_host_client.cpp | 2 +- tests/components/usb_uart/common.yaml | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index 5e9866f381..31bdde2df8 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -65,6 +65,7 @@ static_assert(MAX_REQUESTS >= 1 && MAX_REQUESTS <= 32, "MAX_REQUESTS must be bet // This is tied to the static_assert above, which enforces MAX_REQUESTS is between 1 and 32. // If MAX_REQUESTS is increased above 32, this logic and the static_assert must be updated. using trq_bitmask_t = std::conditional<(MAX_REQUESTS <= 16), uint16_t, uint32_t>::type; +static constexpr trq_bitmask_t ALL_REQUESTS_IN_USE = MAX_REQUESTS == 32 ? ~0 : (1 << MAX_REQUESTS) - 1; static constexpr size_t USB_EVENT_QUEUE_SIZE = 32; // Size of event queue between USB task and main loop static constexpr size_t USB_TASK_STACK_SIZE = 4096; // Stack size for USB task (same as ESP-IDF USB examples) diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index cc0c932503..2456b0c742 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -338,7 +338,7 @@ TransferRequest *USBClient::get_trq_() { // Find first available slot (bit = 0) and try to claim it atomically // We use a while loop to allow retrying the same slot after CAS failure for (;;) { - if (mask == (1 << MAX_REQUESTS) - 1) { + if (mask == ALL_REQUESTS_IN_USE) { ESP_LOGE(TAG, "All %zu transfer slots in use", MAX_REQUESTS); return nullptr; } diff --git a/tests/components/usb_uart/common.yaml b/tests/components/usb_uart/common.yaml index 46ad6291f9..474c3f5c8d 100644 --- a/tests/components/usb_uart/common.yaml +++ b/tests/components/usb_uart/common.yaml @@ -1,3 +1,6 @@ +usb_host: + max_transfer_requests: 32 + usb_uart: - id: uart_0 type: cdc_acm From dfb4b31bf9ee381f5ebe2261b8aeaf1872edebbf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Oct 2025 17:37:40 -0500 Subject: [PATCH 162/394] [template] Store initial option as index in template select (#11523) --- .../components/template/select/__init__.py | 15 +++++++--- .../template/select/template_select.cpp | 28 ++++++++----------- .../template/select/template_select.h | 4 +-- .../host_mode_empty_string_options.yaml | 11 ++++++++ .../test_host_mode_empty_string_options.py | 28 +++++++++++++++++-- 5 files changed, 61 insertions(+), 25 deletions(-) diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py index 3282092d63..0e9c240547 100644 --- a/esphome/components/template/select/__init__.py +++ b/esphome/components/template/select/__init__.py @@ -73,11 +73,18 @@ async def to_code(config): cg.add(var.set_template(template_)) else: - cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) - cg.add(var.set_initial_option(config[CONF_INITIAL_OPTION])) + # Only set if non-default to avoid bloating setup() function + if config[CONF_OPTIMISTIC]: + cg.add(var.set_optimistic(True)) + initial_option_index = config[CONF_OPTIONS].index(config[CONF_INITIAL_OPTION]) + # Only set if non-zero to avoid bloating setup() function + # (initial_option_index_ is zero-initialized in the header) + if initial_option_index != 0: + cg.add(var.set_initial_option_index(initial_option_index)) - if CONF_RESTORE_VALUE in config: - cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) + # Only set if True (default is False) + if config.get(CONF_RESTORE_VALUE): + cg.add(var.set_restore_value(True)) if CONF_SET_ACTION in config: await automation.build_automation( diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 95b0ee0d2b..3765cf02bf 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -10,26 +10,21 @@ void TemplateSelect::setup() { if (this->f_.has_value()) return; - std::string value; - if (!this->restore_value_) { - value = this->initial_option_; - ESP_LOGD(TAG, "State from initial: %s", value.c_str()); - } else { - size_t index; + size_t index = this->initial_option_index_; + if (this->restore_value_) { this->pref_ = global_preferences->make_preference(this->get_preference_hash()); - if (!this->pref_.load(&index)) { - value = this->initial_option_; - ESP_LOGD(TAG, "State from initial (could not load stored index): %s", value.c_str()); - } else if (!this->has_index(index)) { - value = this->initial_option_; - ESP_LOGD(TAG, "State from initial (restored index %d out of bounds): %s", index, value.c_str()); + size_t restored_index; + if (this->pref_.load(&restored_index) && this->has_index(restored_index)) { + index = restored_index; + ESP_LOGD(TAG, "State from restore: %s", this->at(index).value().c_str()); } else { - value = this->at(index).value(); - ESP_LOGD(TAG, "State from restore: %s", value.c_str()); + ESP_LOGD(TAG, "State from initial (could not load or invalid stored index): %s", this->at(index).value().c_str()); } + } else { + ESP_LOGD(TAG, "State from initial: %s", this->at(index).value().c_str()); } - this->publish_state(value); + this->publish_state(this->at(index).value()); } void TemplateSelect::update() { @@ -69,7 +64,8 @@ void TemplateSelect::dump_config() { " Optimistic: %s\n" " Initial Option: %s\n" " Restore Value: %s", - YESNO(this->optimistic_), this->initial_option_.c_str(), YESNO(this->restore_value_)); + YESNO(this->optimistic_), this->at(this->initial_option_index_).value().c_str(), + YESNO(this->restore_value_)); } } // namespace template_ diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index c1b348b26a..e77e4d8f14 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -19,13 +19,13 @@ class TemplateSelect : public select::Select, public PollingComponent { Trigger *get_set_trigger() const { return this->set_trigger_; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } - void set_initial_option(const std::string &initial_option) { this->initial_option_ = initial_option; } + void set_initial_option_index(size_t initial_option_index) { this->initial_option_index_ = initial_option_index; } void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } protected: void control(const std::string &value) override; bool optimistic_ = false; - std::string initial_option_; + size_t initial_option_index_{0}; bool restore_value_ = false; Trigger *set_trigger_ = new Trigger(); optional (*)()> f_; diff --git a/tests/integration/fixtures/host_mode_empty_string_options.yaml b/tests/integration/fixtures/host_mode_empty_string_options.yaml index ab8e6cd005..a170511c46 100644 --- a/tests/integration/fixtures/host_mode_empty_string_options.yaml +++ b/tests/integration/fixtures/host_mode_empty_string_options.yaml @@ -41,6 +41,17 @@ select: - "" # Empty string at the end initial_option: "Choice X" + - platform: template + name: "Select Initial Option Test" + id: select_initial_option_test + optimistic: true + options: + - "First" + - "Second" + - "Third" + - "Fourth" + initial_option: "Third" # Test non-default initial option + # Add a sensor to ensure we have other entities in the list sensor: - platform: template diff --git a/tests/integration/test_host_mode_empty_string_options.py b/tests/integration/test_host_mode_empty_string_options.py index 242db2d40f..1180ce75fc 100644 --- a/tests/integration/test_host_mode_empty_string_options.py +++ b/tests/integration/test_host_mode_empty_string_options.py @@ -36,8 +36,8 @@ async def test_host_mode_empty_string_options( # Find our select entities select_entities = [e for e in entity_info if isinstance(e, SelectInfo)] - assert len(select_entities) == 3, ( - f"Expected 3 select entities, got {len(select_entities)}" + assert len(select_entities) == 4, ( + f"Expected 4 select entities, got {len(select_entities)}" ) # Verify each select entity by name and check their options @@ -71,6 +71,15 @@ async def test_host_mode_empty_string_options( assert empty_last.options[2] == "Choice Z" assert empty_last.options[3] == "" # Empty string at end + # Check "Select Initial Option Test" - verify non-default initial option + assert "Select Initial Option Test" in selects_by_name + initial_option_test = selects_by_name["Select Initial Option Test"] + assert len(initial_option_test.options) == 4 + assert initial_option_test.options[0] == "First" + assert initial_option_test.options[1] == "Second" + assert initial_option_test.options[2] == "Third" + assert initial_option_test.options[3] == "Fourth" + # If we got here without protobuf decoding errors, the fix is working # The bug would have caused "Invalid protobuf message" errors with trailing bytes @@ -78,7 +87,12 @@ async def test_host_mode_empty_string_options( # This ensures empty strings work properly in state messages too states: dict[int, EntityState] = {} states_received_future: asyncio.Future[None] = loop.create_future() - expected_select_keys = {empty_first.key, empty_middle.key, empty_last.key} + expected_select_keys = { + empty_first.key, + empty_middle.key, + empty_last.key, + initial_option_test.key, + } received_select_keys = set() def on_state(state: EntityState) -> None: @@ -109,6 +123,14 @@ async def test_host_mode_empty_string_options( assert empty_first.key in states assert empty_middle.key in states assert empty_last.key in states + assert initial_option_test.key in states + + # Verify the initial option is set correctly to "Third" (not the default "First") + initial_state = states[initial_option_test.key] + assert initial_state.state == "Third", ( + f"Expected initial state 'Third' but got '{initial_state.state}' - " + f"initial_option not correctly applied" + ) # The main test is that we got here without protobuf errors # The select entities with empty string options were properly encoded From ce8a6a6c438716add8256daf3d2d3b95d51fbacd Mon Sep 17 00:00:00 2001 From: Daniel Herrmann Date: Tue, 28 Oct 2025 00:24:13 +0100 Subject: [PATCH 163/394] fix: load_cert_chain requires the path, not a file object (#11543) --- esphome/mqtt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/mqtt.py b/esphome/mqtt.py index f1c631697a..093ee64df4 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -120,7 +120,7 @@ def prepare( cert_file.flush() key_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE_KEY)) key_file.flush() - context.load_cert_chain(cert_file, key_file) + context.load_cert_chain(cert_file.name, key_file.name) client.tls_set_context(context) try: From bdbe9caf3653cec968170d419c02a862f1ee085b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Oct 2025 19:11:32 -0500 Subject: [PATCH 164/394] [modbus_controller] Optimize lambdas to use function pointers instead of std::function --- .../binary_sensor/modbus_binarysensor.h | 4 +- .../modbus_controller/number/modbus_number.h | 8 +- .../modbus_controller/output/modbus_output.h | 8 +- .../modbus_controller/select/modbus_select.h | 11 ++- .../modbus_controller/sensor/modbus_sensor.h | 4 +- .../modbus_controller/switch/modbus_switch.h | 8 +- .../text_sensor/modbus_textsensor.h | 5 +- .../components/modbus_controller/common.yaml | 83 +++++++++++++++++++ 8 files changed, 106 insertions(+), 25 deletions(-) diff --git a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h index 3a017c6f88..119f4fdd5a 100644 --- a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h +++ b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h @@ -33,8 +33,8 @@ class ModbusBinarySensor : public Component, public binary_sensor::BinarySensor, void dump_config() override; - using transform_func_t = std::function(ModbusBinarySensor *, bool, const std::vector &)>; - void set_template(transform_func_t &&f) { this->transform_func_ = f; } + using transform_func_t = optional (*)(ModbusBinarySensor *, bool, const std::vector &); + void set_template(transform_func_t f) { this->transform_func_ = f; } protected: optional transform_func_{nullopt}; diff --git a/esphome/components/modbus_controller/number/modbus_number.h b/esphome/components/modbus_controller/number/modbus_number.h index 8f77b2e014..169f85ff36 100644 --- a/esphome/components/modbus_controller/number/modbus_number.h +++ b/esphome/components/modbus_controller/number/modbus_number.h @@ -31,10 +31,10 @@ class ModbusNumber : public number::Number, public Component, public SensorItem void set_parent(ModbusController *parent) { this->parent_ = parent; } void set_write_multiply(float factor) { this->multiply_by_ = factor; } - using transform_func_t = std::function(ModbusNumber *, float, const std::vector &)>; - using write_transform_func_t = std::function(ModbusNumber *, float, std::vector &)>; - void set_template(transform_func_t &&f) { this->transform_func_ = f; } - void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + using transform_func_t = optional (*)(ModbusNumber *, float, const std::vector &); + using write_transform_func_t = optional (*)(ModbusNumber *, float, std::vector &); + void set_template(transform_func_t f) { this->transform_func_ = f; } + void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; } void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } protected: diff --git a/esphome/components/modbus_controller/output/modbus_output.h b/esphome/components/modbus_controller/output/modbus_output.h index bceb97affb..0fb4bb89ea 100644 --- a/esphome/components/modbus_controller/output/modbus_output.h +++ b/esphome/components/modbus_controller/output/modbus_output.h @@ -29,8 +29,8 @@ class ModbusFloatOutput : public output::FloatOutput, public Component, public S // Do nothing void parse_and_publish(const std::vector &data) override{}; - using write_transform_func_t = std::function(ModbusFloatOutput *, float, std::vector &)>; - void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + using write_transform_func_t = optional (*)(ModbusFloatOutput *, float, std::vector &); + void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; } void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } protected: @@ -60,8 +60,8 @@ class ModbusBinaryOutput : public output::BinaryOutput, public Component, public // Do nothing void parse_and_publish(const std::vector &data) override{}; - using write_transform_func_t = std::function(ModbusBinaryOutput *, bool, std::vector &)>; - void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + using write_transform_func_t = optional (*)(ModbusBinaryOutput *, bool, std::vector &); + void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; } void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } protected: diff --git a/esphome/components/modbus_controller/select/modbus_select.h b/esphome/components/modbus_controller/select/modbus_select.h index 55fb2107dd..e6b98aead2 100644 --- a/esphome/components/modbus_controller/select/modbus_select.h +++ b/esphome/components/modbus_controller/select/modbus_select.h @@ -26,16 +26,15 @@ class ModbusSelect : public Component, public select::Select, public SensorItem this->mapping_ = std::move(mapping); } - using transform_func_t = - std::function(ModbusSelect *const, int64_t, const std::vector &)>; - using write_transform_func_t = - std::function(ModbusSelect *const, const std::string &, int64_t, std::vector &)>; + using transform_func_t = optional (*)(ModbusSelect *const, int64_t, const std::vector &); + using write_transform_func_t = optional (*)(ModbusSelect *const, const std::string &, int64_t, + std::vector &); void set_parent(ModbusController *const parent) { this->parent_ = parent; } void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } - void set_template(transform_func_t &&f) { this->transform_func_ = f; } - void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + void set_template(transform_func_t f) { this->transform_func_ = f; } + void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; } void dump_config() override; void parse_and_publish(const std::vector &data) override; diff --git a/esphome/components/modbus_controller/sensor/modbus_sensor.h b/esphome/components/modbus_controller/sensor/modbus_sensor.h index 65eb487c1c..ba943c873c 100644 --- a/esphome/components/modbus_controller/sensor/modbus_sensor.h +++ b/esphome/components/modbus_controller/sensor/modbus_sensor.h @@ -25,9 +25,9 @@ class ModbusSensor : public Component, public sensor::Sensor, public SensorItem void parse_and_publish(const std::vector &data) override; void dump_config() override; - using transform_func_t = std::function(ModbusSensor *, float, const std::vector &)>; + using transform_func_t = optional (*)(ModbusSensor *, float, const std::vector &); - void set_template(transform_func_t &&f) { this->transform_func_ = f; } + void set_template(transform_func_t f) { this->transform_func_ = f; } protected: optional transform_func_{nullopt}; diff --git a/esphome/components/modbus_controller/switch/modbus_switch.h b/esphome/components/modbus_controller/switch/modbus_switch.h index 0098076ef4..301c2bf548 100644 --- a/esphome/components/modbus_controller/switch/modbus_switch.h +++ b/esphome/components/modbus_controller/switch/modbus_switch.h @@ -34,10 +34,10 @@ class ModbusSwitch : public Component, public switch_::Switch, public SensorItem void parse_and_publish(const std::vector &data) override; void set_parent(ModbusController *parent) { this->parent_ = parent; } - using transform_func_t = std::function(ModbusSwitch *, bool, const std::vector &)>; - using write_transform_func_t = std::function(ModbusSwitch *, bool, std::vector &)>; - void set_template(transform_func_t &&f) { this->publish_transform_func_ = f; } - void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + using transform_func_t = optional (*)(ModbusSwitch *, bool, const std::vector &); + using write_transform_func_t = optional (*)(ModbusSwitch *, bool, std::vector &); + void set_template(transform_func_t f) { this->publish_transform_func_ = f; } + void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; } void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } protected: diff --git a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h index d6eb5fd230..6666aea976 100644 --- a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h +++ b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h @@ -30,9 +30,8 @@ class ModbusTextSensor : public Component, public text_sensor::TextSensor, publi void dump_config() override; void parse_and_publish(const std::vector &data) override; - using transform_func_t = - std::function(ModbusTextSensor *, std::string, const std::vector &)>; - void set_template(transform_func_t &&f) { this->transform_func_ = f; } + using transform_func_t = optional (*)(ModbusTextSensor *, std::string, const std::vector &); + void set_template(transform_func_t f) { this->transform_func_ = f; } protected: optional transform_func_{nullopt}; diff --git a/tests/components/modbus_controller/common.yaml b/tests/components/modbus_controller/common.yaml index ae5520e57d..ffaa1491c5 100644 --- a/tests/components/modbus_controller/common.yaml +++ b/tests/components/modbus_controller/common.yaml @@ -56,6 +56,14 @@ binary_sensor: register_type: read address: 0x3200 bitmask: 0x80 + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_binary_sensor2 + name: Test Binary Sensor with Lambda + register_type: read + address: 0x3201 + lambda: |- + return x; number: - platform: modbus_controller @@ -65,6 +73,16 @@ number: address: 0x9001 value_type: U_WORD multiply: 1.0 + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_number2 + name: Test Number with Lambda + address: 0x9002 + value_type: U_WORD + lambda: |- + return x * 2.0; + write_lambda: |- + return x / 2.0; output: - platform: modbus_controller @@ -74,6 +92,14 @@ output: register_type: holding value_type: U_WORD multiply: 1000 + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_output2 + address: 2049 + register_type: holding + value_type: U_WORD + write_lambda: |- + return x * 100.0; select: - platform: modbus_controller @@ -87,6 +113,34 @@ select: "One": 1 "Two": 2 "Three": 3 + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_select2 + name: Test Select with Lambda + address: 1001 + value_type: U_WORD + optionsmap: + "Off": 0 + "On": 1 + "Two": 2 + lambda: |- + ESP_LOGD("Reg1001", "Received value %lld", x); + if (x > 1) { + return std::string("Two"); + } else if (x == 1) { + return std::string("On"); + } + return std::string("Off"); + write_lambda: |- + ESP_LOGD("Reg1001", "Set option to %s (%lld)", x.c_str(), value); + if (x == "On") { + return 1; + } + if (x == "Two") { + payload.push_back(0x0002); + return 0; + } + return value; sensor: - platform: modbus_controller @@ -97,6 +151,15 @@ sensor: address: 0x9001 unit_of_measurement: "AH" value_type: U_WORD + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_sensor2 + name: Test Sensor with Lambda + register_type: holding + address: 0x9002 + value_type: U_WORD + lambda: |- + return x / 10.0; switch: - platform: modbus_controller @@ -106,6 +169,16 @@ switch: register_type: coil address: 0x15 bitmask: 1 + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_switch2 + name: Test Switch with Lambda + register_type: coil + address: 0x16 + lambda: |- + return !x; + write_lambda: |- + return !x; text_sensor: - platform: modbus_controller @@ -117,3 +190,13 @@ text_sensor: register_count: 3 raw_encode: HEXBYTES response_size: 6 + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_text_sensor2 + name: Test Text Sensor with Lambda + register_type: holding + address: 0x9014 + register_count: 2 + response_size: 4 + lambda: |- + return "Modified: " + x; From 1e9309ffffe10ea5310340ce834fa73ea5145403 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Mon, 27 Oct 2025 17:20:21 -0700 Subject: [PATCH 165/394] [tuya] allow enum for eco id (#11544) Co-authored-by: Samuel Sieb --- esphome/components/tuya/climate/tuya_climate.cpp | 8 +++++++- esphome/components/tuya/climate/tuya_climate.h | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/components/tuya/climate/tuya_climate.cpp b/esphome/components/tuya/climate/tuya_climate.cpp index 04fb14acff..d3c78104e3 100644 --- a/esphome/components/tuya/climate/tuya_climate.cpp +++ b/esphome/components/tuya/climate/tuya_climate.cpp @@ -67,7 +67,9 @@ void TuyaClimate::setup() { } if (this->eco_id_.has_value()) { this->parent_->register_listener(*this->eco_id_, [this](const TuyaDatapoint &datapoint) { + // Whether data type is BOOL or ENUM, it will still be a 1 or a 0, so the functions below are valid in both cases this->eco_ = datapoint.value_bool; + this->eco_type_ = datapoint.type; ESP_LOGV(TAG, "MCU reported eco is: %s", ONOFF(this->eco_)); this->compute_preset_(); this->compute_target_temperature_(); @@ -176,7 +178,11 @@ void TuyaClimate::control(const climate::ClimateCall &call) { if (this->eco_id_.has_value()) { const bool eco = preset == climate::CLIMATE_PRESET_ECO; ESP_LOGV(TAG, "Setting eco: %s", ONOFF(eco)); - this->parent_->set_boolean_datapoint_value(*this->eco_id_, eco); + if (this->eco_type_ == TuyaDatapointType::ENUM) { + this->parent_->set_enum_datapoint_value(*this->eco_id_, eco); + } else { + this->parent_->set_boolean_datapoint_value(*this->eco_id_, eco); + } } if (this->sleep_id_.has_value()) { const bool sleep = preset == climate::CLIMATE_PRESET_SLEEP; diff --git a/esphome/components/tuya/climate/tuya_climate.h b/esphome/components/tuya/climate/tuya_climate.h index d6258c21e1..31bef57639 100644 --- a/esphome/components/tuya/climate/tuya_climate.h +++ b/esphome/components/tuya/climate/tuya_climate.h @@ -104,6 +104,7 @@ class TuyaClimate : public climate::Climate, public Component { optional eco_id_{}; optional sleep_id_{}; optional eco_temperature_{}; + TuyaDatapointType eco_type_{}; uint8_t active_state_; uint8_t fan_state_; optional swing_vertical_id_{}; From 5647f36900ed455f90d827bb0154dfebca660494 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 28 Oct 2025 01:21:17 +0100 Subject: [PATCH 166/394] [nextion] Remove TFT upload baud rate validation to reduce flash usage (#11012) --- esphome/components/nextion/nextion_upload_arduino.cpp | 5 ----- esphome/components/nextion/nextion_upload_idf.cpp | 5 ----- 2 files changed, 10 deletions(-) diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index b0e5d121dd..b4d217d7aa 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -174,11 +174,6 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { // Check if baud rate is supported this->original_baud_rate_ = this->parent_->get_baud_rate(); - static const std::vector SUPPORTED_BAUD_RATES = {2400, 4800, 9600, 19200, 31250, 38400, 57600, - 115200, 230400, 250000, 256000, 512000, 921600}; - if (std::find(SUPPORTED_BAUD_RATES.begin(), SUPPORTED_BAUD_RATES.end(), baud_rate) == SUPPORTED_BAUD_RATES.end()) { - baud_rate = this->original_baud_rate_; - } ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); // Define the configuration for the HTTP client diff --git a/esphome/components/nextion/nextion_upload_idf.cpp b/esphome/components/nextion/nextion_upload_idf.cpp index 78a47f9e2c..3b0d65643d 100644 --- a/esphome/components/nextion/nextion_upload_idf.cpp +++ b/esphome/components/nextion/nextion_upload_idf.cpp @@ -177,11 +177,6 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { // Check if baud rate is supported this->original_baud_rate_ = this->parent_->get_baud_rate(); - static const std::vector SUPPORTED_BAUD_RATES = {2400, 4800, 9600, 19200, 31250, 38400, 57600, - 115200, 230400, 250000, 256000, 512000, 921600}; - if (std::find(SUPPORTED_BAUD_RATES.begin(), SUPPORTED_BAUD_RATES.end(), baud_rate) == SUPPORTED_BAUD_RATES.end()) { - baud_rate = this->original_baud_rate_; - } ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); // Define the configuration for the HTTP client From 285e006637a3bdf6011b3d68a98bf0c19b290f6a Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 28 Oct 2025 01:22:28 +0100 Subject: [PATCH 167/394] [nextion] Add `set_component_visibility()` method for dynamic visibility control (#11530) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/nextion/nextion.h | 17 +++++++++++++++++ esphome/components/nextion/nextion_base.h | 1 + esphome/components/nextion/nextion_commands.cpp | 10 +++++----- .../components/nextion/nextion_component.cpp | 8 +++----- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index e2c4faa1d0..c078ab9d56 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -540,6 +540,23 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe */ void goto_page(uint8_t page); + /** + * Set the visibility of a component. + * + * @param component The component name. + * @param show True to show the component, false to hide it. + * + * @see show_component() + * @see hide_component() + * + * Example: + * ```cpp + * it.set_component_visibility("textview", true); // Equivalent to show_component("textview") + * it.set_component_visibility("textview", false); // Equivalent to hide_component("textview") + * ``` + */ + void set_component_visibility(const char *component, bool show) override; + /** * Hide a component. * @param component The component name. diff --git a/esphome/components/nextion/nextion_base.h b/esphome/components/nextion/nextion_base.h index b88dd399f8..d46cd9a185 100644 --- a/esphome/components/nextion/nextion_base.h +++ b/esphome/components/nextion/nextion_base.h @@ -45,6 +45,7 @@ class NextionBase { virtual void set_component_pressed_font_color(const char *component, Color color) = 0; virtual void set_component_font(const char *component, uint8_t font_id) = 0; + virtual void set_component_visibility(const char *component, bool show) = 0; virtual void show_component(const char *component) = 0; virtual void hide_component(const char *component) = 0; diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index f3a282717b..cfaae7e3e0 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -201,13 +201,13 @@ void Nextion::set_component_font(const char *component, uint8_t font_id) { this->add_no_result_to_queue_with_printf_("set_component_font", "%s.font=%" PRIu8, component, font_id); } -void Nextion::hide_component(const char *component) { - this->add_no_result_to_queue_with_printf_("hide_component", "vis %s,0", component); +void Nextion::set_component_visibility(const char *component, bool show) { + this->add_no_result_to_queue_with_printf_("set_component_visibility", "vis %s,%d", component, show ? 1 : 0); } -void Nextion::show_component(const char *component) { - this->add_no_result_to_queue_with_printf_("show_component", "vis %s,1", component); -} +void Nextion::hide_component(const char *component) { this->set_component_visibility(component, false); } + +void Nextion::show_component(const char *component) { this->set_component_visibility(component, true); } void Nextion::enable_component_touch(const char *component) { this->add_no_result_to_queue_with_printf_("enable_component_touch", "tsw %s,1", component); diff --git a/esphome/components/nextion/nextion_component.cpp b/esphome/components/nextion/nextion_component.cpp index 32929d6845..324ad87372 100644 --- a/esphome/components/nextion/nextion_component.cpp +++ b/esphome/components/nextion/nextion_component.cpp @@ -81,13 +81,11 @@ void NextionComponent::update_component_settings(bool force_update) { this->component_flags_.visible_needs_update = false; - if (this->component_flags_.visible) { - this->nextion_->show_component(name_to_send.c_str()); - this->send_state_to_nextion(); - } else { - this->nextion_->hide_component(name_to_send.c_str()); + this->nextion_->set_component_visibility(name_to_send.c_str(), this->component_flags_.visible); + if (!this->component_flags_.visible) { return; } + this->send_state_to_nextion(); } if (this->component_flags_.bco_needs_update || (force_update && this->component_flags_.bco2_is_set)) { From d2f5fcd20169ef62e61990d9a79f9f1230f4ba81 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Oct 2025 21:15:59 -0500 Subject: [PATCH 168/394] preen --- .../modbus_controller/select/modbus_select.cpp | 2 +- esphome/components/select/select.cpp | 2 ++ esphome/components/select/select.h | 3 +++ esphome/components/template/select/template_select.cpp | 9 ++++----- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/esphome/components/modbus_controller/select/modbus_select.cpp b/esphome/components/modbus_controller/select/modbus_select.cpp index 4d4b5a4ffc..48bf2835f2 100644 --- a/esphome/components/modbus_controller/select/modbus_select.cpp +++ b/esphome/components/modbus_controller/select/modbus_select.cpp @@ -28,7 +28,7 @@ void ModbusSelect::parse_and_publish(const std::vector &data) { if (map_it != this->mapping_.cend()) { size_t idx = std::distance(this->mapping_.cbegin(), map_it); - new_state = std::string(this->traits.get_options()[idx]); + new_state = std::string(this->option_at(idx)); ESP_LOGV(TAG, "Found option %s for value %lld", new_state->c_str(), value); } else { ESP_LOGE(TAG, "No option found for mapping %lld", value); diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 66cd51e15a..5e30be3c13 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -60,5 +60,7 @@ 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 diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 902b8a78ce..eabb39898b 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -56,6 +56,9 @@ class Select : public EntityBase { /// Return the (optional) option value at the provided index offset. optional at(size_t index) const; + /// Return the option value at the provided index offset (as const char* from flash). + const char *option_at(size_t index) const; + void add_on_state_callback(std::function &&callback); protected: diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 3765cf02bf..c7a1d8a344 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -16,12 +16,12 @@ void TemplateSelect::setup() { size_t restored_index; if (this->pref_.load(&restored_index) && this->has_index(restored_index)) { index = restored_index; - ESP_LOGD(TAG, "State from restore: %s", this->at(index).value().c_str()); + ESP_LOGD(TAG, "State from restore: %s", this->option_at(index)); } else { - ESP_LOGD(TAG, "State from initial (could not load or invalid stored index): %s", this->at(index).value().c_str()); + ESP_LOGD(TAG, "State from initial (could not load or invalid stored index): %s", this->option_at(index)); } } else { - ESP_LOGD(TAG, "State from initial: %s", this->at(index).value().c_str()); + ESP_LOGD(TAG, "State from initial: %s", this->option_at(index)); } this->publish_state(this->at(index).value()); @@ -64,8 +64,7 @@ void TemplateSelect::dump_config() { " Optimistic: %s\n" " Initial Option: %s\n" " Restore Value: %s", - YESNO(this->optimistic_), this->at(this->initial_option_index_).value().c_str(), - YESNO(this->restore_value_)); + YESNO(this->optimistic_), this->option_at(this->initial_option_index_), YESNO(this->restore_value_)); } } // namespace template_ From 85205a28d283583d72c2681f89f37c0b2d28f8fa Mon Sep 17 00:00:00 2001 From: aanban Date: Tue, 28 Oct 2025 03:49:16 +0100 Subject: [PATCH 169/394] [remote_base] add support for Dyson cool AM07 tower fan (#10163) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/remote_base/__init__.py | 44 ++++++++++++ .../components/remote_base/dyson_protocol.cpp | 71 +++++++++++++++++++ .../components/remote_base/dyson_protocol.h | 46 ++++++++++++ .../remote_receiver/common-actions.yaml | 5 ++ .../remote_transmitter/common-buttons.yaml | 7 ++ 5 files changed, 173 insertions(+) create mode 100644 esphome/components/remote_base/dyson_protocol.cpp create mode 100644 esphome/components/remote_base/dyson_protocol.h diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index ccf16a8beb..8d735ea563 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -17,6 +17,7 @@ from esphome.const import ( CONF_FAMILY, CONF_GROUP, CONF_ID, + CONF_INDEX, CONF_INVERTED, CONF_LEVEL, CONF_MAGNITUDE, @@ -616,6 +617,49 @@ async def dooya_action(var, config, args): cg.add(var.set_check(template_)) +# Dyson +DysonData, DysonBinarySensor, DysonTrigger, DysonAction, DysonDumper = declare_protocol( + "Dyson" +) +DYSON_SCHEMA = cv.Schema( + { + cv.Required(CONF_CODE): cv.hex_uint16_t, + cv.Optional(CONF_INDEX, default=0xFF): cv.hex_uint8_t, + } +) + + +@register_binary_sensor("dyson", DysonBinarySensor, DYSON_SCHEMA) +def dyson_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + DysonData, + ("code", config[CONF_CODE]), + ("index", config[CONF_INDEX]), + ) + ) + ) + + +@register_trigger("dyson", DysonTrigger, DysonData) +def dyson_trigger(var, config): + pass + + +@register_dumper("dyson", DysonDumper) +def dyson_dumper(var, config): + pass + + +@register_action("dyson", DysonAction, DYSON_SCHEMA) +async def dyson_action(var, config, args): + template_ = await cg.templatable(config[CONF_CODE], args, cg.uint16) + cg.add(var.set_code(template_)) + template_ = await cg.templatable(config[CONF_INDEX], args, cg.uint8) + cg.add(var.set_index(template_)) + + # JVC JVCData, JVCBinarySensor, JVCTrigger, JVCAction, JVCDumper = declare_protocol("JVC") JVC_SCHEMA = cv.Schema({cv.Required(CONF_DATA): cv.hex_uint32_t}) diff --git a/esphome/components/remote_base/dyson_protocol.cpp b/esphome/components/remote_base/dyson_protocol.cpp new file mode 100644 index 0000000000..db4e1135f4 --- /dev/null +++ b/esphome/components/remote_base/dyson_protocol.cpp @@ -0,0 +1,71 @@ +#include "dyson_protocol.h" +#include "esphome/core/log.h" + +#include + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.dyson"; + +// pulsewidth [µs] +constexpr uint32_t PW_MARK_US = 780; +constexpr uint32_t PW_SHORT_US = 720; +constexpr uint32_t PW_LONG_US = 1500; +constexpr uint32_t PW_START_US = 2280; + +// MSB of 15 bit dyson code +constexpr uint16_t MSB_DYSON = (1 << 14); + +// required symbols in transmit buffer = (start_symbol + 15 data_symbols) +constexpr uint32_t N_SYMBOLS_REQ = 2u * (1 + 15); + +void DysonProtocol::encode(RemoteTransmitData *dst, const DysonData &data) { + uint32_t raw_code = (data.code << 2) + (data.index & 3); + dst->set_carrier_frequency(36000); + dst->reserve(N_SYMBOLS_REQ + 1); + dst->item(PW_START_US, PW_SHORT_US); + for (uint16_t mask = MSB_DYSON; mask != 0; mask >>= 1) { + if (mask == (mask & raw_code)) { + dst->item(PW_MARK_US, PW_LONG_US); + } else { + dst->item(PW_MARK_US, PW_SHORT_US); + } + } + dst->mark(PW_MARK_US); // final carrier pulse +} + +optional DysonProtocol::decode(RemoteReceiveData src) { + uint32_t n_received = static_cast(src.size()); + uint16_t raw_code = 0; + DysonData data{ + .code = 0, + .index = 0, + }; + if (n_received < N_SYMBOLS_REQ) + return {}; // invalid frame length + if (!src.expect_item(PW_START_US, PW_SHORT_US)) + return {}; // start not found + for (uint16_t mask = MSB_DYSON; mask != 0; mask >>= 1) { + if (src.expect_item(PW_MARK_US, PW_SHORT_US)) { + raw_code &= ~mask; // zero detected + } else if (src.expect_item(PW_MARK_US, PW_LONG_US)) { + raw_code |= mask; // one detected + } else { + return {}; // invalid data item + } + } + data.code = raw_code >> 2; // extract button code + data.index = raw_code & 3; // extract rolling index + if (src.expect_mark(PW_MARK_US)) { // check total length + return data; + } + return {}; // frame not complete +} + +void DysonProtocol::dump(const DysonData &data) { + ESP_LOGI(TAG, "Dyson: code=0x%x rolling index=%d", data.code, data.index); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/dyson_protocol.h b/esphome/components/remote_base/dyson_protocol.h new file mode 100644 index 0000000000..d1c08fefba --- /dev/null +++ b/esphome/components/remote_base/dyson_protocol.h @@ -0,0 +1,46 @@ +#pragma once + +#include "remote_base.h" + +#include + +namespace esphome { +namespace remote_base { + +static constexpr uint8_t IGNORE_INDEX = 0xFF; + +struct DysonData { + uint16_t code; // the button, e.g. power, swing, fan++, ... + uint8_t index; // the rolling index counter + bool operator==(const DysonData &rhs) const { + if (IGNORE_INDEX == index || IGNORE_INDEX == rhs.index) { + return code == rhs.code; + } + return code == rhs.code && index == rhs.index; + } +}; + +class DysonProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const DysonData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const DysonData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Dyson) + +template class DysonAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint16_t, code) + TEMPLATABLE_VALUE(uint8_t, index) + + void encode(RemoteTransmitData *dst, Ts... x) override { + DysonData data{}; + data.code = this->code_.value(x...); + data.index = this->index_.value(x...); + DysonProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/tests/components/remote_receiver/common-actions.yaml b/tests/components/remote_receiver/common-actions.yaml index c2dc2f0c29..de01fa3602 100644 --- a/tests/components/remote_receiver/common-actions.yaml +++ b/tests/components/remote_receiver/common-actions.yaml @@ -48,6 +48,11 @@ on_drayton: - logger.log: format: "on_drayton: %u %u %u" args: ["x.address", "x.channel", "x.command"] +on_dyson: + then: + - logger.log: + format: "on_dyson: %u %u" + args: ["x.code", "x.index"] on_gobox: then: - logger.log: diff --git a/tests/components/remote_transmitter/common-buttons.yaml b/tests/components/remote_transmitter/common-buttons.yaml index 58127d1ab4..e9593cc97c 100644 --- a/tests/components/remote_transmitter/common-buttons.yaml +++ b/tests/components/remote_transmitter/common-buttons.yaml @@ -6,6 +6,13 @@ button: remote_transmitter.transmit_beo4: source: 0x01 command: 0x0C + - platform: template + name: Dyson fan up + id: dyson_fan_up + on_press: + remote_transmitter.transmit_dyson: + code: 0x1215 + index: 0x0 - platform: template name: JVC Off id: living_room_lights_on From fc660bbb66a7e54f535585aad311d0d61fbdd134 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Oct 2025 22:32:04 -0500 Subject: [PATCH 170/394] [esp32_ble_server][esp32_improv]: Eliminate unnecessary heap allocations --- esphome/components/esp32_ble_server/__init__.py | 4 +++- .../esp32_ble_server/ble_characteristic.cpp | 11 ++++++++--- .../components/esp32_ble_server/ble_characteristic.h | 3 ++- .../components/esp32_ble_server/ble_descriptor.cpp | 8 +++++--- esphome/components/esp32_ble_server/ble_descriptor.h | 5 ++++- .../esp32_improv/esp32_improv_component.cpp | 10 ++++------ .../components/esp32_improv/esp32_improv_component.h | 2 +- 7 files changed, 27 insertions(+), 16 deletions(-) diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index 55310f3275..a7e2522fac 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -461,7 +461,9 @@ async def parse_value(value_config, args): if isinstance(value, str): value = list(value.encode(value_config[CONF_STRING_ENCODING])) if isinstance(value, list): - return cg.std_vector.template(cg.uint8)(value) + # Generate initializer list {1, 2, 3} instead of std::vector({1, 2, 3}) + # This calls the set_value(std::initializer_list) overload + return cg.ArrayInitializer(*value) val = cg.RawExpression(f"{value_config[CONF_TYPE]}({cg.safe_exp(value)})") return ByteBuffer_ns.wrap(val, value_config[CONF_ENDIANNESS]) diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index 87f562a250..7627a58338 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -35,13 +35,18 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); } -void BLECharacteristic::set_value(const std::vector &buffer) { +void BLECharacteristic::set_value(std::vector &&buffer) { xSemaphoreTake(this->set_value_lock_, 0L); - this->value_ = buffer; + this->value_ = std::move(buffer); xSemaphoreGive(this->set_value_lock_); } + +void BLECharacteristic::set_value(std::initializer_list data) { + this->set_value(std::vector(data)); // Delegate to move overload +} + void BLECharacteristic::set_value(const std::string &buffer) { - this->set_value(std::vector(buffer.begin(), buffer.end())); + this->set_value(std::vector(buffer.begin(), buffer.end())); // Delegate to move overload } void BLECharacteristic::notify() { diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index 7cceec0ef1..b913915789 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -33,7 +33,8 @@ class BLECharacteristic { ~BLECharacteristic(); void set_value(ByteBuffer buffer); - void set_value(const std::vector &buffer); + void set_value(std::vector &&buffer); + void set_value(std::initializer_list data); void set_value(const std::string &buffer); void set_broadcast_property(bool value); diff --git a/esphome/components/esp32_ble_server/ble_descriptor.cpp b/esphome/components/esp32_ble_server/ble_descriptor.cpp index 16941cca0f..2d053c09bd 100644 --- a/esphome/components/esp32_ble_server/ble_descriptor.cpp +++ b/esphome/components/esp32_ble_server/ble_descriptor.cpp @@ -46,15 +46,17 @@ void BLEDescriptor::do_create(BLECharacteristic *characteristic) { this->state_ = CREATING; } -void BLEDescriptor::set_value(std::vector buffer) { - size_t length = buffer.size(); +void BLEDescriptor::set_value(std::vector &&buffer) { this->set_value_impl_(buffer.data(), buffer.size()); } +void BLEDescriptor::set_value(std::initializer_list data) { this->set_value_impl_(data.begin(), data.size()); } + +void BLEDescriptor::set_value_impl_(const uint8_t *data, size_t length) { if (length > this->value_.attr_max_len) { ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", length, this->value_.attr_max_len); return; } this->value_.attr_len = length; - memcpy(this->value_.attr_value, buffer.data(), length); + memcpy(this->value_.attr_value, data, length); } void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, diff --git a/esphome/components/esp32_ble_server/ble_descriptor.h b/esphome/components/esp32_ble_server/ble_descriptor.h index 425462a316..5f4f146d6f 100644 --- a/esphome/components/esp32_ble_server/ble_descriptor.h +++ b/esphome/components/esp32_ble_server/ble_descriptor.h @@ -27,7 +27,8 @@ class BLEDescriptor { void do_create(BLECharacteristic *characteristic); ESPBTUUID get_uuid() const { return this->uuid_; } - void set_value(std::vector buffer); + void set_value(std::vector &&buffer); + void set_value(std::initializer_list data); void set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); } void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); @@ -42,6 +43,8 @@ class BLEDescriptor { } protected: + void set_value_impl_(const uint8_t *data, size_t length); + BLECharacteristic *characteristic_{nullptr}; ESPBTUUID uuid_; uint16_t handle_{0xFFFF}; diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 56436b9d3d..2fa9d8f523 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -270,8 +270,8 @@ void ESP32ImprovComponent::set_error_(improv::Error error) { } } -void ESP32ImprovComponent::send_response_(std::vector &response) { - this->rpc_response_->set_value(ByteBuffer::wrap(response)); +void ESP32ImprovComponent::send_response_(std::vector &&response) { + this->rpc_response_->set_value(std::move(response)); if (this->state_ != improv::STATE_STOPPED) this->rpc_response_->notify(); } @@ -409,10 +409,8 @@ void ESP32ImprovComponent::check_wifi_connection_() { } } #endif - // Pass to build_rpc_response using vector constructor from iterators to avoid extra copies - std::vector data = improv::build_rpc_response( - improv::WIFI_SETTINGS, std::vector(url_strings, url_strings + url_count)); - this->send_response_(data); + this->send_response_(improv::build_rpc_response(improv::WIFI_SETTINGS, + std::vector(url_strings, url_strings + url_count))); } else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) { ESP_LOGD(TAG, "WiFi provisioned externally"); } diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index fd3b2b861d..989552ea56 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -109,7 +109,7 @@ class ESP32ImprovComponent : public Component, public improv_base::ImprovBase { void set_state_(improv::State state, bool update_advertising = true); void set_error_(improv::Error error); improv::State get_initial_state_() const; - void send_response_(std::vector &response); + void send_response_(std::vector &&response); void process_incoming_data_(); void on_wifi_connect_timeout_(); void check_wifi_connection_(); From aba72809d3b280f62aa12a4129e2fbce48b304f7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Oct 2025 22:43:10 -0500 Subject: [PATCH 171/394] Additional tests for ble_client lambdas (#11565) --- tests/components/ble_client/common.yaml | 49 +++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/components/ble_client/common.yaml b/tests/components/ble_client/common.yaml index b5272d01f0..aa4b639463 100644 --- a/tests/components/ble_client/common.yaml +++ b/tests/components/ble_client/common.yaml @@ -3,3 +3,52 @@ esp32_ble_tracker: ble_client: - mac_address: 01:02:03:04:05:06 id: test_blec + on_connect: + - ble_client.ble_write: + id: test_blec + service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678" + characteristic_uuid: "abcd1235-abcd-1234-abcd-abcd12345678" + value: !lambda |- + return std::vector{0x01, 0x02, 0x03}; + - ble_client.ble_write: + id: test_blec + service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678" + characteristic_uuid: "abcd1235-abcd-1234-abcd-abcd12345678" + value: [0x04, 0x05, 0x06] + on_passkey_request: + - ble_client.passkey_reply: + id: test_blec + passkey: !lambda |- + return 123456; + - ble_client.passkey_reply: + id: test_blec + passkey: 654321 + on_numeric_comparison_request: + - ble_client.numeric_comparison_reply: + id: test_blec + accept: !lambda |- + return true; + - ble_client.numeric_comparison_reply: + id: test_blec + accept: false + +sensor: + - platform: ble_client + ble_client_id: test_blec + type: characteristic + id: test_sensor_lambda + name: "BLE Sensor with Lambda" + service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678" + characteristic_uuid: "abcd1236-abcd-1234-abcd-abcd12345678" + lambda: |- + if (x.size() >= 2) { + return (float)(x[0] | (x[1] << 8)) / 100.0; + } + return NAN; + - platform: ble_client + ble_client_id: test_blec + type: characteristic + id: test_sensor_no_lambda + name: "BLE Sensor without Lambda" + service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678" + characteristic_uuid: "abcd1237-abcd-1234-abcd-abcd12345678" From f3b69383fdf25f8a473343790108575c182f8d36 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Oct 2025 22:43:16 -0500 Subject: [PATCH 172/394] Add additional modbus compile tests (#11567) --- .../components/modbus_controller/common.yaml | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/tests/components/modbus_controller/common.yaml b/tests/components/modbus_controller/common.yaml index ae5520e57d..ffaa1491c5 100644 --- a/tests/components/modbus_controller/common.yaml +++ b/tests/components/modbus_controller/common.yaml @@ -56,6 +56,14 @@ binary_sensor: register_type: read address: 0x3200 bitmask: 0x80 + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_binary_sensor2 + name: Test Binary Sensor with Lambda + register_type: read + address: 0x3201 + lambda: |- + return x; number: - platform: modbus_controller @@ -65,6 +73,16 @@ number: address: 0x9001 value_type: U_WORD multiply: 1.0 + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_number2 + name: Test Number with Lambda + address: 0x9002 + value_type: U_WORD + lambda: |- + return x * 2.0; + write_lambda: |- + return x / 2.0; output: - platform: modbus_controller @@ -74,6 +92,14 @@ output: register_type: holding value_type: U_WORD multiply: 1000 + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_output2 + address: 2049 + register_type: holding + value_type: U_WORD + write_lambda: |- + return x * 100.0; select: - platform: modbus_controller @@ -87,6 +113,34 @@ select: "One": 1 "Two": 2 "Three": 3 + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_select2 + name: Test Select with Lambda + address: 1001 + value_type: U_WORD + optionsmap: + "Off": 0 + "On": 1 + "Two": 2 + lambda: |- + ESP_LOGD("Reg1001", "Received value %lld", x); + if (x > 1) { + return std::string("Two"); + } else if (x == 1) { + return std::string("On"); + } + return std::string("Off"); + write_lambda: |- + ESP_LOGD("Reg1001", "Set option to %s (%lld)", x.c_str(), value); + if (x == "On") { + return 1; + } + if (x == "Two") { + payload.push_back(0x0002); + return 0; + } + return value; sensor: - platform: modbus_controller @@ -97,6 +151,15 @@ sensor: address: 0x9001 unit_of_measurement: "AH" value_type: U_WORD + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_sensor2 + name: Test Sensor with Lambda + register_type: holding + address: 0x9002 + value_type: U_WORD + lambda: |- + return x / 10.0; switch: - platform: modbus_controller @@ -106,6 +169,16 @@ switch: register_type: coil address: 0x15 bitmask: 1 + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_switch2 + name: Test Switch with Lambda + register_type: coil + address: 0x16 + lambda: |- + return !x; + write_lambda: |- + return !x; text_sensor: - platform: modbus_controller @@ -117,3 +190,13 @@ text_sensor: register_count: 3 raw_encode: HEXBYTES response_size: 6 + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_text_sensor2 + name: Test Text Sensor with Lambda + register_type: holding + address: 0x9014 + register_count: 2 + response_size: 4 + lambda: |- + return "Modified: " + x; From f5e32d03d01d2a32008f8fdb331e15febf5dd8ea Mon Sep 17 00:00:00 2001 From: rwrozelle Date: Tue, 28 Oct 2025 12:41:48 -0400 Subject: [PATCH 173/394] [http_request] update timeout to be uint32_t (#11577) --- esphome/components/http_request/http_request.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 40c85d51ed..5010cf47a0 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -124,7 +124,7 @@ class HttpRequestComponent : public Component { float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void set_useragent(const char *useragent) { this->useragent_ = useragent; } - void set_timeout(uint16_t timeout) { this->timeout_ = timeout; } + void set_timeout(uint32_t timeout) { this->timeout_ = timeout; } void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; } uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; } void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; } @@ -173,7 +173,7 @@ class HttpRequestComponent : public Component { const char *useragent_{nullptr}; bool follow_redirects_{}; uint16_t redirect_limit_{}; - uint16_t timeout_{4500}; + uint32_t timeout_{4500}; uint32_t watchdog_timeout_{0}; }; From da19673f51682a0898e5fe167e52eb9d3f9f6ed4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Oct 2025 14:03:09 -0500 Subject: [PATCH 174/394] Add additional uart test coverage (#11571) --- tests/components/uart/test.esp32-idf.yaml | 38 +++++++++++++++++++++ tests/components/uart/test.esp8266-ard.yaml | 18 ++++++++++ 2 files changed, 56 insertions(+) diff --git a/tests/components/uart/test.esp32-idf.yaml b/tests/components/uart/test.esp32-idf.yaml index 5634c5c6f6..9744a48409 100644 --- a/tests/components/uart/test.esp32-idf.yaml +++ b/tests/components/uart/test.esp32-idf.yaml @@ -19,3 +19,41 @@ uart: packet_transport: - platform: uart + +switch: + # Test uart switch with single state (array) + - platform: uart + name: "UART Switch Single Array" + uart_id: uart_uart + data: [0x01, 0x02, 0x03] + # Test uart switch with single state (string) + - platform: uart + name: "UART Switch Single String" + uart_id: uart_uart + data: "ON" + # Test uart switch with turn_on/turn_off (arrays) + - platform: uart + name: "UART Switch Dual Array" + uart_id: uart_uart + data: + turn_on: [0xA0, 0xA1, 0xA2] + turn_off: [0xB0, 0xB1, 0xB2] + # Test uart switch with turn_on/turn_off (strings) + - platform: uart + name: "UART Switch Dual String" + uart_id: uart_uart + data: + turn_on: "TURN_ON" + turn_off: "TURN_OFF" + +button: + # Test uart button with array data + - platform: uart + name: "UART Button Array" + uart_id: uart_uart + data: [0xFF, 0xEE, 0xDD] + # Test uart button with string data + - platform: uart + name: "UART Button String" + uart_id: uart_uart + data: "BUTTON_PRESS" diff --git a/tests/components/uart/test.esp8266-ard.yaml b/tests/components/uart/test.esp8266-ard.yaml index 09178f1663..566038ee3e 100644 --- a/tests/components/uart/test.esp8266-ard.yaml +++ b/tests/components/uart/test.esp8266-ard.yaml @@ -13,3 +13,21 @@ uart: rx_buffer_size: 512 parity: EVEN stop_bits: 2 + +switch: + - platform: uart + name: "UART Switch Array" + uart_id: uart_uart + data: [0x01, 0x02, 0x03] + - platform: uart + name: "UART Switch Dual" + uart_id: uart_uart + data: + turn_on: [0xA0, 0xA1] + turn_off: [0xB0, 0xB1] + +button: + - platform: uart + name: "UART Button" + uart_id: uart_uart + data: [0xFF, 0xEE] From 7dd829cfcaf07bd98512c97f6523bc3c22063b92 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Oct 2025 14:05:12 -0500 Subject: [PATCH 175/394] [esp32_ble_server][esp32_improv] Eliminate unnecessary heap allocations (#11569) --- esphome/components/esp32_ble_server/__init__.py | 4 +++- .../esp32_ble_server/ble_characteristic.cpp | 11 ++++++++--- .../components/esp32_ble_server/ble_characteristic.h | 3 ++- .../components/esp32_ble_server/ble_descriptor.cpp | 8 +++++--- esphome/components/esp32_ble_server/ble_descriptor.h | 5 ++++- .../esp32_improv/esp32_improv_component.cpp | 10 ++++------ .../components/esp32_improv/esp32_improv_component.h | 2 +- 7 files changed, 27 insertions(+), 16 deletions(-) diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index 55310f3275..a7e2522fac 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -461,7 +461,9 @@ async def parse_value(value_config, args): if isinstance(value, str): value = list(value.encode(value_config[CONF_STRING_ENCODING])) if isinstance(value, list): - return cg.std_vector.template(cg.uint8)(value) + # Generate initializer list {1, 2, 3} instead of std::vector({1, 2, 3}) + # This calls the set_value(std::initializer_list) overload + return cg.ArrayInitializer(*value) val = cg.RawExpression(f"{value_config[CONF_TYPE]}({cg.safe_exp(value)})") return ByteBuffer_ns.wrap(val, value_config[CONF_ENDIANNESS]) diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index 87f562a250..7627a58338 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -35,13 +35,18 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); } -void BLECharacteristic::set_value(const std::vector &buffer) { +void BLECharacteristic::set_value(std::vector &&buffer) { xSemaphoreTake(this->set_value_lock_, 0L); - this->value_ = buffer; + this->value_ = std::move(buffer); xSemaphoreGive(this->set_value_lock_); } + +void BLECharacteristic::set_value(std::initializer_list data) { + this->set_value(std::vector(data)); // Delegate to move overload +} + void BLECharacteristic::set_value(const std::string &buffer) { - this->set_value(std::vector(buffer.begin(), buffer.end())); + this->set_value(std::vector(buffer.begin(), buffer.end())); // Delegate to move overload } void BLECharacteristic::notify() { diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index 7cceec0ef1..b913915789 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -33,7 +33,8 @@ class BLECharacteristic { ~BLECharacteristic(); void set_value(ByteBuffer buffer); - void set_value(const std::vector &buffer); + void set_value(std::vector &&buffer); + void set_value(std::initializer_list data); void set_value(const std::string &buffer); void set_broadcast_property(bool value); diff --git a/esphome/components/esp32_ble_server/ble_descriptor.cpp b/esphome/components/esp32_ble_server/ble_descriptor.cpp index 16941cca0f..2d053c09bd 100644 --- a/esphome/components/esp32_ble_server/ble_descriptor.cpp +++ b/esphome/components/esp32_ble_server/ble_descriptor.cpp @@ -46,15 +46,17 @@ void BLEDescriptor::do_create(BLECharacteristic *characteristic) { this->state_ = CREATING; } -void BLEDescriptor::set_value(std::vector buffer) { - size_t length = buffer.size(); +void BLEDescriptor::set_value(std::vector &&buffer) { this->set_value_impl_(buffer.data(), buffer.size()); } +void BLEDescriptor::set_value(std::initializer_list data) { this->set_value_impl_(data.begin(), data.size()); } + +void BLEDescriptor::set_value_impl_(const uint8_t *data, size_t length) { if (length > this->value_.attr_max_len) { ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", length, this->value_.attr_max_len); return; } this->value_.attr_len = length; - memcpy(this->value_.attr_value, buffer.data(), length); + memcpy(this->value_.attr_value, data, length); } void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, diff --git a/esphome/components/esp32_ble_server/ble_descriptor.h b/esphome/components/esp32_ble_server/ble_descriptor.h index 425462a316..5f4f146d6f 100644 --- a/esphome/components/esp32_ble_server/ble_descriptor.h +++ b/esphome/components/esp32_ble_server/ble_descriptor.h @@ -27,7 +27,8 @@ class BLEDescriptor { void do_create(BLECharacteristic *characteristic); ESPBTUUID get_uuid() const { return this->uuid_; } - void set_value(std::vector buffer); + void set_value(std::vector &&buffer); + void set_value(std::initializer_list data); void set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); } void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); @@ -42,6 +43,8 @@ class BLEDescriptor { } protected: + void set_value_impl_(const uint8_t *data, size_t length); + BLECharacteristic *characteristic_{nullptr}; ESPBTUUID uuid_; uint16_t handle_{0xFFFF}; diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 56436b9d3d..2fa9d8f523 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -270,8 +270,8 @@ void ESP32ImprovComponent::set_error_(improv::Error error) { } } -void ESP32ImprovComponent::send_response_(std::vector &response) { - this->rpc_response_->set_value(ByteBuffer::wrap(response)); +void ESP32ImprovComponent::send_response_(std::vector &&response) { + this->rpc_response_->set_value(std::move(response)); if (this->state_ != improv::STATE_STOPPED) this->rpc_response_->notify(); } @@ -409,10 +409,8 @@ void ESP32ImprovComponent::check_wifi_connection_() { } } #endif - // Pass to build_rpc_response using vector constructor from iterators to avoid extra copies - std::vector data = improv::build_rpc_response( - improv::WIFI_SETTINGS, std::vector(url_strings, url_strings + url_count)); - this->send_response_(data); + this->send_response_(improv::build_rpc_response(improv::WIFI_SETTINGS, + std::vector(url_strings, url_strings + url_count))); } else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) { ESP_LOGD(TAG, "WiFi provisioned externally"); } diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index fd3b2b861d..989552ea56 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -109,7 +109,7 @@ class ESP32ImprovComponent : public Component, public improv_base::ImprovBase { void set_state_(improv::State state, bool update_advertising = true); void set_error_(improv::Error error); improv::State get_initial_state_() const; - void send_response_(std::vector &response); + void send_response_(std::vector &&response); void process_incoming_data_(); void on_wifi_connect_timeout_(); void check_wifi_connection_(); From c3f40de844516c7eeca2eab48c866cd3092f8967 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Oct 2025 14:06:13 -0500 Subject: [PATCH 176/394] [modbus_controller] Optimize lambdas to use function pointers instead of std::function (#11566) --- .../binary_sensor/modbus_binarysensor.h | 4 ++-- .../modbus_controller/number/modbus_number.h | 8 ++++---- .../modbus_controller/output/modbus_output.h | 8 ++++---- .../modbus_controller/select/modbus_select.h | 11 +++++------ .../modbus_controller/sensor/modbus_sensor.h | 4 ++-- .../modbus_controller/switch/modbus_switch.h | 8 ++++---- .../modbus_controller/text_sensor/modbus_textsensor.h | 5 ++--- 7 files changed, 23 insertions(+), 25 deletions(-) diff --git a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h index 3a017c6f88..119f4fdd5a 100644 --- a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h +++ b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h @@ -33,8 +33,8 @@ class ModbusBinarySensor : public Component, public binary_sensor::BinarySensor, void dump_config() override; - using transform_func_t = std::function(ModbusBinarySensor *, bool, const std::vector &)>; - void set_template(transform_func_t &&f) { this->transform_func_ = f; } + using transform_func_t = optional (*)(ModbusBinarySensor *, bool, const std::vector &); + void set_template(transform_func_t f) { this->transform_func_ = f; } protected: optional transform_func_{nullopt}; diff --git a/esphome/components/modbus_controller/number/modbus_number.h b/esphome/components/modbus_controller/number/modbus_number.h index 8f77b2e014..169f85ff36 100644 --- a/esphome/components/modbus_controller/number/modbus_number.h +++ b/esphome/components/modbus_controller/number/modbus_number.h @@ -31,10 +31,10 @@ class ModbusNumber : public number::Number, public Component, public SensorItem void set_parent(ModbusController *parent) { this->parent_ = parent; } void set_write_multiply(float factor) { this->multiply_by_ = factor; } - using transform_func_t = std::function(ModbusNumber *, float, const std::vector &)>; - using write_transform_func_t = std::function(ModbusNumber *, float, std::vector &)>; - void set_template(transform_func_t &&f) { this->transform_func_ = f; } - void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + using transform_func_t = optional (*)(ModbusNumber *, float, const std::vector &); + using write_transform_func_t = optional (*)(ModbusNumber *, float, std::vector &); + void set_template(transform_func_t f) { this->transform_func_ = f; } + void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; } void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } protected: diff --git a/esphome/components/modbus_controller/output/modbus_output.h b/esphome/components/modbus_controller/output/modbus_output.h index bceb97affb..0fb4bb89ea 100644 --- a/esphome/components/modbus_controller/output/modbus_output.h +++ b/esphome/components/modbus_controller/output/modbus_output.h @@ -29,8 +29,8 @@ class ModbusFloatOutput : public output::FloatOutput, public Component, public S // Do nothing void parse_and_publish(const std::vector &data) override{}; - using write_transform_func_t = std::function(ModbusFloatOutput *, float, std::vector &)>; - void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + using write_transform_func_t = optional (*)(ModbusFloatOutput *, float, std::vector &); + void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; } void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } protected: @@ -60,8 +60,8 @@ class ModbusBinaryOutput : public output::BinaryOutput, public Component, public // Do nothing void parse_and_publish(const std::vector &data) override{}; - using write_transform_func_t = std::function(ModbusBinaryOutput *, bool, std::vector &)>; - void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + using write_transform_func_t = optional (*)(ModbusBinaryOutput *, bool, std::vector &); + void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; } void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } protected: diff --git a/esphome/components/modbus_controller/select/modbus_select.h b/esphome/components/modbus_controller/select/modbus_select.h index 55fb2107dd..e6b98aead2 100644 --- a/esphome/components/modbus_controller/select/modbus_select.h +++ b/esphome/components/modbus_controller/select/modbus_select.h @@ -26,16 +26,15 @@ class ModbusSelect : public Component, public select::Select, public SensorItem this->mapping_ = std::move(mapping); } - using transform_func_t = - std::function(ModbusSelect *const, int64_t, const std::vector &)>; - using write_transform_func_t = - std::function(ModbusSelect *const, const std::string &, int64_t, std::vector &)>; + using transform_func_t = optional (*)(ModbusSelect *const, int64_t, const std::vector &); + using write_transform_func_t = optional (*)(ModbusSelect *const, const std::string &, int64_t, + std::vector &); void set_parent(ModbusController *const parent) { this->parent_ = parent; } void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } - void set_template(transform_func_t &&f) { this->transform_func_ = f; } - void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + void set_template(transform_func_t f) { this->transform_func_ = f; } + void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; } void dump_config() override; void parse_and_publish(const std::vector &data) override; diff --git a/esphome/components/modbus_controller/sensor/modbus_sensor.h b/esphome/components/modbus_controller/sensor/modbus_sensor.h index 65eb487c1c..ba943c873c 100644 --- a/esphome/components/modbus_controller/sensor/modbus_sensor.h +++ b/esphome/components/modbus_controller/sensor/modbus_sensor.h @@ -25,9 +25,9 @@ class ModbusSensor : public Component, public sensor::Sensor, public SensorItem void parse_and_publish(const std::vector &data) override; void dump_config() override; - using transform_func_t = std::function(ModbusSensor *, float, const std::vector &)>; + using transform_func_t = optional (*)(ModbusSensor *, float, const std::vector &); - void set_template(transform_func_t &&f) { this->transform_func_ = f; } + void set_template(transform_func_t f) { this->transform_func_ = f; } protected: optional transform_func_{nullopt}; diff --git a/esphome/components/modbus_controller/switch/modbus_switch.h b/esphome/components/modbus_controller/switch/modbus_switch.h index 0098076ef4..301c2bf548 100644 --- a/esphome/components/modbus_controller/switch/modbus_switch.h +++ b/esphome/components/modbus_controller/switch/modbus_switch.h @@ -34,10 +34,10 @@ class ModbusSwitch : public Component, public switch_::Switch, public SensorItem void parse_and_publish(const std::vector &data) override; void set_parent(ModbusController *parent) { this->parent_ = parent; } - using transform_func_t = std::function(ModbusSwitch *, bool, const std::vector &)>; - using write_transform_func_t = std::function(ModbusSwitch *, bool, std::vector &)>; - void set_template(transform_func_t &&f) { this->publish_transform_func_ = f; } - void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + using transform_func_t = optional (*)(ModbusSwitch *, bool, const std::vector &); + using write_transform_func_t = optional (*)(ModbusSwitch *, bool, std::vector &); + void set_template(transform_func_t f) { this->publish_transform_func_ = f; } + void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; } void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } protected: diff --git a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h index d6eb5fd230..6666aea976 100644 --- a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h +++ b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h @@ -30,9 +30,8 @@ class ModbusTextSensor : public Component, public text_sensor::TextSensor, publi void dump_config() override; void parse_and_publish(const std::vector &data) override; - using transform_func_t = - std::function(ModbusTextSensor *, std::string, const std::vector &)>; - void set_template(transform_func_t &&f) { this->transform_func_ = f; } + using transform_func_t = optional (*)(ModbusTextSensor *, std::string, const std::vector &); + void set_template(transform_func_t f) { this->transform_func_ = f; } protected: optional transform_func_{nullopt}; From 0119e17f0425bdb8655fe2ad171ca2cd29eb805c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Oct 2025 14:08:13 -0500 Subject: [PATCH 177/394] [ci] Remove base bus components exclusion from memory impact analysis (#11572) --- script/determine-jobs.py | 17 ++++++++------- tests/script/test_determine_jobs.py | 32 ++++++++++++++++++----------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/script/determine-jobs.py b/script/determine-jobs.py index ac384d74f1..21eb529f33 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -48,7 +48,6 @@ import sys from typing import Any from helpers import ( - BASE_BUS_COMPONENTS, CPP_FILE_EXTENSIONS, PYTHON_FILE_EXTENSIONS, changed_files, @@ -453,7 +452,7 @@ def detect_memory_impact_config( # Get actually changed files (not dependencies) files = changed_files(branch) - # Find all changed components (excluding core and base bus components) + # Find all changed components (excluding core) # Also collect platform hints from platform-specific filenames changed_component_set: set[str] = set() has_core_cpp_changes = False @@ -462,13 +461,13 @@ def detect_memory_impact_config( for file in files: component = get_component_from_path(file) if component: - # Skip base bus components as they're used across many builds - if component not in BASE_BUS_COMPONENTS: - changed_component_set.add(component) - # Check if this is a platform-specific file - platform_hint = _detect_platform_hint_from_filename(file) - if platform_hint: - platform_hints.append(platform_hint) + # Add all changed components, including base bus components + # Base bus components (uart, i2c, spi, etc.) should still be analyzed + # when directly changed, even though they're also used as dependencies + changed_component_set.add(component) + # Check if this is a platform-specific file + if platform_hint := _detect_platform_hint_from_filename(file): + platform_hints.append(platform_hint) elif file.startswith("esphome/") and file.endswith(CPP_FILE_EXTENSIONS): # Core ESPHome C++ files changed (not component-specific) # Only C++ files affect memory usage diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index c9ccf53252..c8ef76184f 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -849,39 +849,47 @@ def test_detect_memory_impact_config_no_components_with_tests(tmp_path: Path) -> assert result["should_run"] == "false" -def test_detect_memory_impact_config_skips_base_bus_components(tmp_path: Path) -> None: - """Test that base bus components (i2c, spi, uart) are skipped.""" +def test_detect_memory_impact_config_includes_base_bus_components( + tmp_path: Path, +) -> None: + """Test that base bus components (i2c, spi, uart) are included when directly changed. + + Base bus components should be analyzed for memory impact when they are directly + changed, even though they are often used as dependencies. This ensures that + optimizations to base components (like using move semantics or initializer_list) + are properly measured. + """ # Create test directory structure tests_dir = tmp_path / "tests" / "components" - # i2c component (should be skipped as it's a base bus component) - i2c_dir = tests_dir / "i2c" - i2c_dir.mkdir(parents=True) - (i2c_dir / "test.esp32-idf.yaml").write_text("test: i2c") + # uart component (base bus component that should be included) + uart_dir = tests_dir / "uart" + uart_dir.mkdir(parents=True) + (uart_dir / "test.esp32-idf.yaml").write_text("test: uart") - # wifi component (should not be skipped) + # wifi component (regular component) wifi_dir = tests_dir / "wifi" wifi_dir.mkdir(parents=True) (wifi_dir / "test.esp32-idf.yaml").write_text("test: wifi") - # Mock changed_files to return both i2c and wifi + # Mock changed_files to return both uart and wifi with ( patch.object(determine_jobs, "root_path", str(tmp_path)), patch.object(helpers, "root_path", str(tmp_path)), patch.object(determine_jobs, "changed_files") as mock_changed_files, ): mock_changed_files.return_value = [ - "esphome/components/i2c/i2c.cpp", + "esphome/components/uart/automation.h", # Header file with inline code "esphome/components/wifi/wifi.cpp", ] determine_jobs._component_has_tests.cache_clear() result = determine_jobs.detect_memory_impact_config() - # Should only include wifi, not i2c + # Should include both uart and wifi assert result["should_run"] == "true" - assert result["components"] == ["wifi"] - assert "i2c" not in result["components"] + assert set(result["components"]) == {"uart", "wifi"} + assert result["platform"] == "esp32-idf" # Common platform def test_detect_memory_impact_config_with_variant_tests(tmp_path: Path) -> None: From 08b845455503cd5bc9b6b73c154bffb2255ee44b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Oct 2025 14:10:32 -0500 Subject: [PATCH 178/394] [ble_client] Use function pointers for lambda actions and sensors (#11564) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/ble_client/automation.h | 75 ++++++++++++------- .../ble_client/sensor/ble_sensor.cpp | 4 +- .../components/ble_client/sensor/ble_sensor.h | 10 ++- 3 files changed, 57 insertions(+), 32 deletions(-) diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index a5c661e2f5..55f1cb2f46 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -96,8 +96,11 @@ template class BLEClientWriteAction : public Action, publ BLEClientWriteAction(BLEClient *ble_client) { ble_client->register_ble_node(this); ble_client_ = ble_client; + this->construct_simple_value_(); } + ~BLEClientWriteAction() { this->destroy_simple_value_(); } + void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } @@ -106,14 +109,18 @@ template class BLEClientWriteAction : public Action, publ void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } - void set_value_template(std::function(Ts...)> func) { - this->value_template_ = std::move(func); - has_simple_value_ = false; + void set_value_template(std::vector (*func)(Ts...)) { + this->destroy_simple_value_(); + this->value_.template_func = func; + this->has_simple_value_ = false; } void set_value_simple(const std::vector &value) { - this->value_simple_ = value; - has_simple_value_ = true; + if (!this->has_simple_value_) { + this->construct_simple_value_(); + } + this->value_.simple = value; + this->has_simple_value_ = true; } void play(Ts... x) override {} @@ -121,7 +128,7 @@ template class BLEClientWriteAction : public Action, publ void play_complex(Ts... x) override { this->num_running_++; this->var_ = std::make_tuple(x...); - auto value = this->has_simple_value_ ? this->value_simple_ : this->value_template_(x...); + auto value = this->has_simple_value_ ? this->value_.simple : this->value_.template_func(x...); // on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work. if (!write(value)) this->play_next_(x...); @@ -194,10 +201,22 @@ template class BLEClientWriteAction : public Action, publ } private: + void construct_simple_value_() { new (&this->value_.simple) std::vector(); } + + void destroy_simple_value_() { + if (this->has_simple_value_) { + this->value_.simple.~vector(); + } + } + BLEClient *ble_client_; bool has_simple_value_ = true; - std::vector value_simple_; - std::function(Ts...)> value_template_{}; + union Value { + std::vector simple; + std::vector (*template_func)(Ts...); + Value() {} // trivial constructor + ~Value() {} // trivial destructor - we manage lifetime via discriminator + } value_; espbt::ESPBTUUID service_uuid_; espbt::ESPBTUUID char_uuid_; std::tuple var_{}; @@ -213,9 +232,9 @@ template class BLEClientPasskeyReplyAction : public Actionvalue_simple_; + passkey = this->value_.simple; } else { - passkey = this->value_template_(x...); + passkey = this->value_.template_func(x...); } if (passkey > 999999) return; @@ -224,21 +243,23 @@ template class BLEClientPasskeyReplyAction : public Action func) { - this->value_template_ = std::move(func); - has_simple_value_ = false; + void set_value_template(uint32_t (*func)(Ts...)) { + this->value_.template_func = func; + this->has_simple_value_ = false; } void set_value_simple(const uint32_t &value) { - this->value_simple_ = value; - has_simple_value_ = true; + this->value_.simple = value; + this->has_simple_value_ = true; } private: BLEClient *parent_{nullptr}; bool has_simple_value_ = true; - uint32_t value_simple_{0}; - std::function value_template_{}; + union { + uint32_t simple; + uint32_t (*template_func)(Ts...); + } value_{.simple = 0}; }; template class BLEClientNumericComparisonReplyAction : public Action { @@ -249,27 +270,29 @@ template class BLEClientNumericComparisonReplyAction : public Ac esp_bd_addr_t remote_bda; memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t)); if (has_simple_value_) { - esp_ble_confirm_reply(remote_bda, this->value_simple_); + esp_ble_confirm_reply(remote_bda, this->value_.simple); } else { - esp_ble_confirm_reply(remote_bda, this->value_template_(x...)); + esp_ble_confirm_reply(remote_bda, this->value_.template_func(x...)); } } - void set_value_template(std::function func) { - this->value_template_ = std::move(func); - has_simple_value_ = false; + void set_value_template(bool (*func)(Ts...)) { + this->value_.template_func = func; + this->has_simple_value_ = false; } void set_value_simple(const bool &value) { - this->value_simple_ = value; - has_simple_value_ = true; + this->value_.simple = value; + this->has_simple_value_ = true; } private: BLEClient *parent_{nullptr}; bool has_simple_value_ = true; - bool value_simple_{false}; - std::function value_template_{}; + union { + bool simple; + bool (*template_func)(Ts...); + } value_{.simple = false}; }; template class BLEClientRemoveBondAction : public Action { diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp index d0ccfe1f2e..6d293528c6 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -117,9 +117,9 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga } float BLESensor::parse_data_(uint8_t *value, uint16_t value_len) { - if (this->data_to_value_func_.has_value()) { + if (this->has_data_to_value_) { std::vector data(value, value + value_len); - return (*this->data_to_value_func_)(data); + return this->data_to_value_func_(data); } else { return value[0]; } diff --git a/esphome/components/ble_client/sensor/ble_sensor.h b/esphome/components/ble_client/sensor/ble_sensor.h index 24d1ed2fd2..c6335d5836 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.h +++ b/esphome/components/ble_client/sensor/ble_sensor.h @@ -15,8 +15,6 @@ namespace ble_client { namespace espbt = esphome::esp32_ble_tracker; -using data_to_value_t = std::function)>; - class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClientNode { public: void loop() override; @@ -33,13 +31,17 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie void set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } - void set_data_to_value(data_to_value_t &&lambda) { this->data_to_value_func_ = lambda; } + void set_data_to_value(float (*lambda)(const std::vector &)) { + this->data_to_value_func_ = lambda; + this->has_data_to_value_ = true; + } void set_enable_notify(bool notify) { this->notify_ = notify; } uint16_t handle; protected: float parse_data_(uint8_t *value, uint16_t value_len); - optional data_to_value_func_{}; + bool has_data_to_value_{false}; + float (*data_to_value_func_)(const std::vector &){}; bool notify_; espbt::ESPBTUUID service_uuid_; espbt::ESPBTUUID char_uuid_; From 7ed7e7ad262853dcd553b36fbc9844212f703d6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Oct 2025 14:46:44 -0500 Subject: [PATCH 179/394] [climate] Replace std::set with FiniteSetMask for trait storage (#11466) --- esphome/components/api/api.proto | 12 +- esphome/components/api/api_connection.cpp | 12 +- esphome/components/api/api_pb2.h | 12 +- esphome/components/bedjet/bedjet_const.h | 3 +- .../bedjet/climate/bedjet_climate.h | 2 +- esphome/components/climate/climate.cpp | 4 +- esphome/components/climate/climate_mode.h | 12 +- esphome/components/climate/climate_traits.h | 103 ++++++++++-------- esphome/components/climate_ir/climate_ir.h | 18 +-- esphome/components/haier/haier_base.cpp | 6 +- esphome/components/haier/haier_base.h | 7 +- esphome/components/haier/hon_climate.cpp | 10 +- esphome/components/heatpumpir/heatpumpir.h | 11 +- esphome/components/midea/air_conditioner.h | 23 ++-- .../thermostat/thermostat_climate.h | 4 + esphome/components/toshiba/toshiba.cpp | 2 +- esphome/components/toshiba/toshiba.h | 6 +- .../components/tuya/climate/tuya_climate.cpp | 14 +-- tests/integration/state_utils.py | 6 + .../test_host_mode_climate_basic_state.py | 34 +++--- 20 files changed, 160 insertions(+), 141 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index d202486cfa..fae0f2e75a 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -989,7 +989,7 @@ message ListEntitiesClimateResponse { bool supports_current_temperature = 5; // Deprecated: use feature_flags bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags - repeated ClimateMode supported_modes = 7 [(container_pointer) = "std::set"]; + repeated ClimateMode supported_modes = 7 [(container_pointer_no_template) = "climate::ClimateModeMask"]; float visual_min_temperature = 8; float visual_max_temperature = 9; float visual_target_temperature_step = 10; @@ -998,11 +998,11 @@ message ListEntitiesClimateResponse { // Deprecated in API version 1.5 bool legacy_supports_away = 11 [deprecated=true]; bool supports_action = 12; // Deprecated: use feature_flags - repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer) = "std::set"]; - repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer) = "std::set"]; - repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::set"]; - repeated ClimatePreset supported_presets = 16 [(container_pointer) = "std::set"]; - repeated string supported_custom_presets = 17 [(container_pointer) = "std::set"]; + repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer_no_template) = "climate::ClimateFanModeMask"]; + repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"]; + repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::vector"]; + repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"]; + repeated string supported_custom_presets = 17 [(container_pointer) = "std::vector"]; bool disabled_by_default = 18; string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 20; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index f76080253d..382c4acc16 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -669,18 +669,18 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION); // Current feature flags and other supported parameters msg.feature_flags = traits.get_feature_flags(); - msg.supported_modes = &traits.get_supported_modes_for_api_(); + msg.supported_modes = &traits.get_supported_modes(); msg.visual_min_temperature = traits.get_visual_min_temperature(); msg.visual_max_temperature = traits.get_visual_max_temperature(); msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); msg.visual_min_humidity = traits.get_visual_min_humidity(); msg.visual_max_humidity = traits.get_visual_max_humidity(); - msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_(); - msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_(); - msg.supported_presets = &traits.get_supported_presets_for_api_(); - msg.supported_custom_presets = &traits.get_supported_custom_presets_for_api_(); - msg.supported_swing_modes = &traits.get_supported_swing_modes_for_api_(); + msg.supported_fan_modes = &traits.get_supported_fan_modes(); + msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes(); + msg.supported_presets = &traits.get_supported_presets(); + msg.supported_custom_presets = &traits.get_supported_custom_presets(); + msg.supported_swing_modes = &traits.get_supported_swing_modes(); return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index ed49498176..3e9a10c1f7 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1377,16 +1377,16 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { #endif bool supports_current_temperature{false}; bool supports_two_point_target_temperature{false}; - const std::set *supported_modes{}; + const climate::ClimateModeMask *supported_modes{}; float visual_min_temperature{0.0f}; float visual_max_temperature{0.0f}; float visual_target_temperature_step{0.0f}; bool supports_action{false}; - const std::set *supported_fan_modes{}; - const std::set *supported_swing_modes{}; - const std::set *supported_custom_fan_modes{}; - const std::set *supported_presets{}; - const std::set *supported_custom_presets{}; + const climate::ClimateFanModeMask *supported_fan_modes{}; + const climate::ClimateSwingModeMask *supported_swing_modes{}; + const std::vector *supported_custom_fan_modes{}; + const climate::ClimatePresetMask *supported_presets{}; + const std::vector *supported_custom_presets{}; float visual_current_temperature_step{0.0f}; bool supports_current_humidity{false}; bool supports_target_humidity{false}; diff --git a/esphome/components/bedjet/bedjet_const.h b/esphome/components/bedjet/bedjet_const.h index 7cac1b61ff..0693be1092 100644 --- a/esphome/components/bedjet/bedjet_const.h +++ b/esphome/components/bedjet/bedjet_const.h @@ -99,9 +99,8 @@ enum BedjetCommand : uint8_t { static const uint8_t BEDJET_FAN_SPEED_COUNT = 20; -static const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; +static constexpr const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; -static const std::set BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_; } // namespace bedjet } // namespace esphome diff --git a/esphome/components/bedjet/climate/bedjet_climate.h b/esphome/components/bedjet/climate/bedjet_climate.h index 963f2e585a..dbbb73aeae 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.h +++ b/esphome/components/bedjet/climate/bedjet_climate.h @@ -43,7 +43,7 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli }); // It would be better if we had a slider for the fan modes. - traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES_SET); + traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES); traits.set_supported_presets({ // If we support NONE, then have to decide what happens if the user switches to it (turn off?) // climate::CLIMATE_PRESET_NONE, diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 19fe241729..944934edbf 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -385,7 +385,7 @@ void Climate::save_state_() { if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) { state.uses_custom_fan_mode = true; const auto &supported = traits.get_supported_custom_fan_modes(); - // std::set has consistent order (lexicographic for strings) + // std::vector maintains insertion order size_t i = 0; for (const auto &mode : supported) { if (mode == custom_fan_mode) { @@ -402,7 +402,7 @@ void Climate::save_state_() { if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) { state.uses_custom_preset = true; const auto &supported = traits.get_supported_custom_presets(); - // std::set has consistent order (lexicographic for strings) + // std::vector maintains insertion order size_t i = 0; for (const auto &preset : supported) { if (preset == custom_preset) { diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index faec5d2537..44423d2f22 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -7,6 +7,7 @@ namespace esphome { namespace climate { /// Enum for all modes a climate device can be in. +/// NOTE: If adding values, update ClimateModeMask in climate_traits.h to use the new last value enum ClimateMode : uint8_t { /// The climate device is off CLIMATE_MODE_OFF = 0, @@ -24,7 +25,7 @@ enum ClimateMode : uint8_t { * For example, the target temperature can be adjusted based on a schedule, or learned behavior. * The target temperature can't be adjusted when in this mode. */ - CLIMATE_MODE_AUTO = 6 + CLIMATE_MODE_AUTO = 6 // Update ClimateModeMask in climate_traits.h if adding values after this }; /// Enum for the current action of the climate device. Values match those of ClimateMode. @@ -43,6 +44,7 @@ enum ClimateAction : uint8_t { CLIMATE_ACTION_FAN = 6, }; +/// NOTE: If adding values, update ClimateFanModeMask in climate_traits.h to use the new last value enum ClimateFanMode : uint8_t { /// The fan mode is set to On CLIMATE_FAN_ON = 0, @@ -63,10 +65,11 @@ enum ClimateFanMode : uint8_t { /// The fan mode is set to Diffuse CLIMATE_FAN_DIFFUSE = 8, /// The fan mode is set to Quiet - CLIMATE_FAN_QUIET = 9, + CLIMATE_FAN_QUIET = 9, // Update ClimateFanModeMask in climate_traits.h if adding values after this }; /// Enum for all modes a climate swing can be in +/// NOTE: If adding values, update ClimateSwingModeMask in climate_traits.h to use the new last value enum ClimateSwingMode : uint8_t { /// The swing mode is set to Off CLIMATE_SWING_OFF = 0, @@ -75,10 +78,11 @@ enum ClimateSwingMode : uint8_t { /// The fan mode is set to Vertical CLIMATE_SWING_VERTICAL = 2, /// The fan mode is set to Horizontal - CLIMATE_SWING_HORIZONTAL = 3, + CLIMATE_SWING_HORIZONTAL = 3, // Update ClimateSwingModeMask in climate_traits.h if adding values after this }; /// Enum for all preset modes +/// NOTE: If adding values, update ClimatePresetMask in climate_traits.h to use the new last value enum ClimatePreset : uint8_t { /// No preset is active CLIMATE_PRESET_NONE = 0, @@ -95,7 +99,7 @@ enum ClimatePreset : uint8_t { /// Device is prepared for sleep CLIMATE_PRESET_SLEEP = 6, /// Device is reacting to activity (e.g., movement sensors) - CLIMATE_PRESET_ACTIVITY = 7, + CLIMATE_PRESET_ACTIVITY = 7, // Update ClimatePresetMask in climate_traits.h if adding values after this }; enum ClimateFeature : uint32_t { diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 2962a147d7..1161a54f4e 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -1,19 +1,33 @@ #pragma once -#include +#include #include "climate_mode.h" +#include "esphome/core/finite_set_mask.h" #include "esphome/core/helpers.h" namespace esphome { - -#ifdef USE_API -namespace api { -class APIConnection; -} // namespace api -#endif - namespace climate { +// Type aliases for climate enum bitmasks +// These replace std::set to eliminate red-black tree overhead +// For contiguous enums starting at 0, DefaultBitPolicy provides 1:1 mapping (enum value = bit position) +// Bitmask size is automatically calculated from the last enum value +using ClimateModeMask = FiniteSetMask>; +using ClimateFanModeMask = FiniteSetMask>; +using ClimateSwingModeMask = + FiniteSetMask>; +using ClimatePresetMask = FiniteSetMask>; + +// Lightweight linear search for small vectors (1-20 items) +// Avoids std::find template overhead +template inline bool vector_contains(const std::vector &vec, const T &value) { + for (const auto &item : vec) { + if (item == value) + return true; + } + return false; +} + /** This class contains all static data for climate devices. * * All climate devices must support these features: @@ -107,48 +121,60 @@ class ClimateTraits { } } - void set_supported_modes(std::set modes) { this->supported_modes_ = std::move(modes); } + void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); } bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); } - const std::set &get_supported_modes() const { return this->supported_modes_; } + const ClimateModeMask &get_supported_modes() const { return this->supported_modes_; } - void set_supported_fan_modes(std::set modes) { this->supported_fan_modes_ = std::move(modes); } + void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; } void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); } - void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.insert(mode); } + void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.push_back(mode); } bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); } bool get_supports_fan_modes() const { return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty(); } - const std::set &get_supported_fan_modes() const { return this->supported_fan_modes_; } + const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; } - void set_supported_custom_fan_modes(std::set supported_custom_fan_modes) { + void set_supported_custom_fan_modes(std::vector supported_custom_fan_modes) { this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes); } - const std::set &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } + void set_supported_custom_fan_modes(std::initializer_list modes) { + this->supported_custom_fan_modes_ = modes; + } + template void set_supported_custom_fan_modes(const char *const (&modes)[N]) { + this->supported_custom_fan_modes_.assign(modes, modes + N); + } + const std::vector &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { - return this->supported_custom_fan_modes_.count(custom_fan_mode); + return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode); } - void set_supported_presets(std::set presets) { this->supported_presets_ = std::move(presets); } + void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); } - void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.insert(preset); } + void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.push_back(preset); } bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); } bool get_supports_presets() const { return !this->supported_presets_.empty(); } - const std::set &get_supported_presets() const { return this->supported_presets_; } + const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; } - void set_supported_custom_presets(std::set supported_custom_presets) { + void set_supported_custom_presets(std::vector supported_custom_presets) { this->supported_custom_presets_ = std::move(supported_custom_presets); } - const std::set &get_supported_custom_presets() const { return this->supported_custom_presets_; } + void set_supported_custom_presets(std::initializer_list presets) { + this->supported_custom_presets_ = presets; + } + template void set_supported_custom_presets(const char *const (&presets)[N]) { + this->supported_custom_presets_.assign(presets, presets + N); + } + const std::vector &get_supported_custom_presets() const { return this->supported_custom_presets_; } bool supports_custom_preset(const std::string &custom_preset) const { - return this->supported_custom_presets_.count(custom_preset); + return vector_contains(this->supported_custom_presets_, custom_preset); } - void set_supported_swing_modes(std::set modes) { this->supported_swing_modes_ = std::move(modes); } + void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); } bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); } bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); } - const std::set &get_supported_swing_modes() const { return this->supported_swing_modes_; } + const ClimateSwingModeMask &get_supported_swing_modes() const { return this->supported_swing_modes_; } float get_visual_min_temperature() const { return this->visual_min_temperature_; } void set_visual_min_temperature(float visual_min_temperature) { @@ -179,23 +205,6 @@ class ClimateTraits { void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; } protected: -#ifdef USE_API - // The API connection is a friend class to access internal methods - friend class api::APIConnection; - // These methods return references to internal data structures. - // They are used by the API to avoid copying data when encoding messages. - // Warning: Do not use these methods outside of the API connection code. - // They return references to internal data that can be invalidated. - const std::set &get_supported_modes_for_api_() const { return this->supported_modes_; } - const std::set &get_supported_fan_modes_for_api_() const { return this->supported_fan_modes_; } - const std::set &get_supported_custom_fan_modes_for_api_() const { - return this->supported_custom_fan_modes_; - } - const std::set &get_supported_presets_for_api_() const { return this->supported_presets_; } - const std::set &get_supported_custom_presets_for_api_() const { return this->supported_custom_presets_; } - const std::set &get_supported_swing_modes_for_api_() const { return this->supported_swing_modes_; } -#endif - void set_mode_support_(climate::ClimateMode mode, bool supported) { if (supported) { this->supported_modes_.insert(mode); @@ -226,12 +235,12 @@ class ClimateTraits { float visual_min_humidity_{30}; float visual_max_humidity_{99}; - std::set supported_modes_ = {climate::CLIMATE_MODE_OFF}; - std::set supported_fan_modes_; - std::set supported_swing_modes_; - std::set supported_presets_; - std::set supported_custom_fan_modes_; - std::set supported_custom_presets_; + climate::ClimateModeMask supported_modes_{climate::CLIMATE_MODE_OFF}; + climate::ClimateFanModeMask supported_fan_modes_; + climate::ClimateSwingModeMask supported_swing_modes_; + climate::ClimatePresetMask supported_presets_; + std::vector supported_custom_fan_modes_; + std::vector supported_custom_presets_; }; } // namespace climate diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index ea0656121f..62a43f0b2d 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -24,16 +24,18 @@ class ClimateIR : public Component, public remote_base::RemoteTransmittable { public: ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f, - bool supports_dry = false, bool supports_fan_only = false, std::set fan_modes = {}, - std::set swing_modes = {}, std::set presets = {}) { + bool supports_dry = false, bool supports_fan_only = false, + climate::ClimateFanModeMask fan_modes = climate::ClimateFanModeMask(), + climate::ClimateSwingModeMask swing_modes = climate::ClimateSwingModeMask(), + climate::ClimatePresetMask presets = climate::ClimatePresetMask()) { this->minimum_temperature_ = minimum_temperature; this->maximum_temperature_ = maximum_temperature; this->temperature_step_ = temperature_step; this->supports_dry_ = supports_dry; this->supports_fan_only_ = supports_fan_only; - this->fan_modes_ = std::move(fan_modes); - this->swing_modes_ = std::move(swing_modes); - this->presets_ = std::move(presets); + this->fan_modes_ = fan_modes; + this->swing_modes_ = swing_modes; + this->presets_ = presets; } void setup() override; @@ -60,9 +62,9 @@ class ClimateIR : public Component, bool supports_heat_{true}; bool supports_dry_{false}; bool supports_fan_only_{false}; - std::set fan_modes_ = {}; - std::set swing_modes_ = {}; - std::set presets_ = {}; + climate::ClimateFanModeMask fan_modes_{}; + climate::ClimateSwingModeMask swing_modes_{}; + climate::ClimatePresetMask presets_{}; sensor::Sensor *sensor_{nullptr}; }; diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index 5709b8e9b5..cd2673a272 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -171,7 +171,7 @@ void HaierClimateBase::toggle_power() { PendingAction({ActionRequest::TOGGLE_POWER, esphome::optional()}); } -void HaierClimateBase::set_supported_swing_modes(const std::set &modes) { +void HaierClimateBase::set_supported_swing_modes(climate::ClimateSwingModeMask modes) { this->traits_.set_supported_swing_modes(modes); if (!modes.empty()) this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); @@ -179,13 +179,13 @@ void HaierClimateBase::set_supported_swing_modes(const std::sethaier_protocol_.set_answer_timeout(timeout); } -void HaierClimateBase::set_supported_modes(const std::set &modes) { +void HaierClimateBase::set_supported_modes(climate::ClimateModeMask modes) { this->traits_.set_supported_modes(modes); this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF); // Always available this->traits_.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); // Always available } -void HaierClimateBase::set_supported_presets(const std::set &presets) { +void HaierClimateBase::set_supported_presets(climate::ClimatePresetMask presets) { this->traits_.set_supported_presets(presets); if (!presets.empty()) this->traits_.add_supported_preset(climate::CLIMATE_PRESET_NONE); diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h index f0597c49ff..e24217bfd9 100644 --- a/esphome/components/haier/haier_base.h +++ b/esphome/components/haier/haier_base.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include "esphome/components/climate/climate.h" #include "esphome/components/uart/uart.h" #include "esphome/core/automation.h" @@ -60,9 +59,9 @@ class HaierClimateBase : public esphome::Component, void send_power_off_command(); void toggle_power(); void reset_protocol() { this->reset_protocol_request_ = true; }; - void set_supported_modes(const std::set &modes); - void set_supported_swing_modes(const std::set &modes); - void set_supported_presets(const std::set &presets); + void set_supported_modes(esphome::climate::ClimateModeMask modes); + void set_supported_swing_modes(esphome::climate::ClimateSwingModeMask modes); + void set_supported_presets(esphome::climate::ClimatePresetMask presets); bool valid_connection() const { return this->protocol_phase_ >= ProtocolPhases::IDLE; }; size_t available() noexcept override { return esphome::uart::UARTDevice::available(); }; size_t read_array(uint8_t *data, size_t len) noexcept override { diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index 76558f2ebb..23d28bfd47 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -1033,9 +1033,9 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * { // Swing mode ClimateSwingMode old_swing_mode = this->swing_mode; - const std::set &swing_modes = traits_.get_supported_swing_modes(); - bool vertical_swing_supported = swing_modes.find(CLIMATE_SWING_VERTICAL) != swing_modes.end(); - bool horizontal_swing_supported = swing_modes.find(CLIMATE_SWING_HORIZONTAL) != swing_modes.end(); + const auto &swing_modes = traits_.get_supported_swing_modes(); + bool vertical_swing_supported = swing_modes.count(CLIMATE_SWING_VERTICAL); + bool horizontal_swing_supported = swing_modes.count(CLIMATE_SWING_HORIZONTAL); if (horizontal_swing_supported && (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)) { if (vertical_swing_supported && @@ -1218,13 +1218,13 @@ void HonClimate::fill_control_messages_queue_() { (uint8_t) hon_protocol::DataParameters::QUIET_MODE, quiet_mode_buf, 2); } - if ((fast_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_BOOST) != presets.end()))) { + if ((fast_mode_buf[1] != 0xFF) && presets.count(climate::ClimatePreset::CLIMATE_PRESET_BOOST)) { this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint8_t) hon_protocol::DataParameters::FAST_MODE, fast_mode_buf, 2); } - if ((away_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_AWAY) != presets.end()))) { + if ((away_mode_buf[1] != 0xFF) && presets.count(climate::ClimatePreset::CLIMATE_PRESET_AWAY)) { this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint8_t) hon_protocol::DataParameters::TEN_DEGREE, diff --git a/esphome/components/heatpumpir/heatpumpir.h b/esphome/components/heatpumpir/heatpumpir.h index 3e14c11861..ed43ffdc83 100644 --- a/esphome/components/heatpumpir/heatpumpir.h +++ b/esphome/components/heatpumpir/heatpumpir.h @@ -97,12 +97,11 @@ const float TEMP_MAX = 100; // Celsius class HeatpumpIRClimate : public climate_ir::ClimateIR { public: HeatpumpIRClimate() - : climate_ir::ClimateIR( - TEMP_MIN, TEMP_MAX, 1.0f, true, true, - std::set{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, - climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_AUTO}, - std::set{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL, - climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {} + : climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true, + {climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH, + climate::CLIMATE_FAN_AUTO}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL, + climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {} void setup() override; void set_protocol(Protocol protocol) { this->protocol_ = protocol; } void set_horizontal_default(HorizontalDirection horizontal_direction) { diff --git a/esphome/components/midea/air_conditioner.h b/esphome/components/midea/air_conditioner.h index e70bd34e71..6c2401efe7 100644 --- a/esphome/components/midea/air_conditioner.h +++ b/esphome/components/midea/air_conditioner.h @@ -19,6 +19,9 @@ using climate::ClimateTraits; using climate::ClimateMode; using climate::ClimateSwingMode; using climate::ClimateFanMode; +using climate::ClimateModeMask; +using climate::ClimateSwingModeMask; +using climate::ClimatePresetMask; class AirConditioner : public ApplianceBase, public climate::Climate { public: @@ -40,20 +43,20 @@ class AirConditioner : public ApplianceBase, void do_power_on() { this->base_.setPowerState(true); } void do_power_off() { this->base_.setPowerState(false); } void do_power_toggle() { this->base_.setPowerState(this->mode == ClimateMode::CLIMATE_MODE_OFF); } - void set_supported_modes(const std::set &modes) { this->supported_modes_ = modes; } - void set_supported_swing_modes(const std::set &modes) { this->supported_swing_modes_ = modes; } - void set_supported_presets(const std::set &presets) { this->supported_presets_ = presets; } - void set_custom_presets(const std::set &presets) { this->supported_custom_presets_ = presets; } - void set_custom_fan_modes(const std::set &modes) { this->supported_custom_fan_modes_ = modes; } + void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } + void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } + void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } + void set_custom_presets(const std::vector &presets) { this->supported_custom_presets_ = presets; } + void set_custom_fan_modes(const std::vector &modes) { this->supported_custom_fan_modes_ = modes; } protected: void control(const ClimateCall &call) override; ClimateTraits traits() override; - std::set supported_modes_{}; - std::set supported_swing_modes_{}; - std::set supported_presets_{}; - std::set supported_custom_presets_{}; - std::set supported_custom_fan_modes_{}; + ClimateModeMask supported_modes_{}; + ClimateSwingModeMask supported_swing_modes_{}; + ClimatePresetMask supported_presets_{}; + std::vector supported_custom_presets_{}; + std::vector supported_custom_fan_modes_{}; Sensor *outdoor_sensor_{nullptr}; Sensor *humidity_sensor_{nullptr}; Sensor *power_sensor_{nullptr}; diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 363d2b09fc..42adab7751 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -40,6 +40,10 @@ enum OnBootRestoreFrom : uint8_t { }; struct ThermostatClimateTimer { + ThermostatClimateTimer() = default; + ThermostatClimateTimer(bool active, uint32_t time, uint32_t started, std::function func) + : active(active), time(time), started(started), func(std::move(func)) {} + bool active; uint32_t time; uint32_t started; diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index 36e5a21ffa..5efa70d6b4 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -405,7 +405,7 @@ void ToshibaClimate::setup() { this->swing_modes_ = this->toshiba_swing_modes_(); // Ensure swing mode is always initialized to a valid value - if (this->swing_modes_.empty() || this->swing_modes_.find(this->swing_mode) == this->swing_modes_.end()) { + if (this->swing_modes_.empty() || !this->swing_modes_.count(this->swing_mode)) { // No swing support for this model or current swing mode not supported, reset to OFF this->swing_mode = climate::CLIMATE_SWING_OFF; } diff --git a/esphome/components/toshiba/toshiba.h b/esphome/components/toshiba/toshiba.h index d76833f406..ee1dec5cc9 100644 --- a/esphome/components/toshiba/toshiba.h +++ b/esphome/components/toshiba/toshiba.h @@ -71,10 +71,10 @@ class ToshibaClimate : public climate_ir::ClimateIR { return TOSHIBA_RAS_2819T_TEMP_C_MAX; return TOSHIBA_GENERIC_TEMP_C_MAX; // Default to GENERIC for unknown models } - std::set toshiba_swing_modes_() { + climate::ClimateSwingModeMask toshiba_swing_modes_() { return (this->model_ == MODEL_GENERIC) - ? std::set{} - : std::set{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}; + ? climate::ClimateSwingModeMask() + : climate::ClimateSwingModeMask{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}; } void encode_(remote_base::RemoteTransmitData *data, const uint8_t *message, uint8_t nbytes, uint8_t repeat); bool decode_(remote_base::RemoteReceiveData *data, uint8_t *message, uint8_t nbytes); diff --git a/esphome/components/tuya/climate/tuya_climate.cpp b/esphome/components/tuya/climate/tuya_climate.cpp index d3c78104e3..4d8fd4b310 100644 --- a/esphome/components/tuya/climate/tuya_climate.cpp +++ b/esphome/components/tuya/climate/tuya_climate.cpp @@ -312,18 +312,12 @@ climate::ClimateTraits TuyaClimate::traits() { traits.add_supported_preset(climate::CLIMATE_PRESET_NONE); } if (this->swing_vertical_id_.has_value() && this->swing_horizontal_id_.has_value()) { - std::set supported_swing_modes = { - climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL, - climate::CLIMATE_SWING_HORIZONTAL}; - traits.set_supported_swing_modes(std::move(supported_swing_modes)); + traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, + climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL}); } else if (this->swing_vertical_id_.has_value()) { - std::set supported_swing_modes = {climate::CLIMATE_SWING_OFF, - climate::CLIMATE_SWING_VERTICAL}; - traits.set_supported_swing_modes(std::move(supported_swing_modes)); + traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}); } else if (this->swing_horizontal_id_.has_value()) { - std::set supported_swing_modes = {climate::CLIMATE_SWING_OFF, - climate::CLIMATE_SWING_HORIZONTAL}; - traits.set_supported_swing_modes(std::move(supported_swing_modes)); + traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL}); } if (fan_speed_id_) { diff --git a/tests/integration/state_utils.py b/tests/integration/state_utils.py index 58d6d2790f..6434a41ddf 100644 --- a/tests/integration/state_utils.py +++ b/tests/integration/state_utils.py @@ -44,6 +44,7 @@ class InitialStateHelper: helper = InitialStateHelper(entities) client.subscribe_states(helper.on_state_wrapper(user_callback)) await helper.wait_for_initial_states() + # Access initial states via helper.initial_states[key] """ def __init__(self, entities: list[EntityInfo]) -> None: @@ -63,6 +64,8 @@ class InitialStateHelper: self._entities_by_id = { (entity.device_id, entity.key): entity for entity in entities } + # Store initial states by key for test access + self.initial_states: dict[int, EntityState] = {} # Log all entities _LOGGER.debug( @@ -127,6 +130,9 @@ class InitialStateHelper: # If this entity is waiting for initial state if entity_id in self._wait_initial_states: + # Store the initial state for test access + self.initial_states[state.key] = state + # Remove from waiting set self._wait_initial_states.discard(entity_id) diff --git a/tests/integration/test_host_mode_climate_basic_state.py b/tests/integration/test_host_mode_climate_basic_state.py index 4697342a99..7d871ed5a8 100644 --- a/tests/integration/test_host_mode_climate_basic_state.py +++ b/tests/integration/test_host_mode_climate_basic_state.py @@ -2,12 +2,11 @@ from __future__ import annotations -import asyncio - import aioesphomeapi -from aioesphomeapi import ClimateAction, ClimateMode, ClimatePreset, EntityState +from aioesphomeapi import ClimateAction, ClimateInfo, ClimateMode, ClimatePreset import pytest +from .state_utils import InitialStateHelper from .types import APIClientConnectedFactory, RunCompiledFunction @@ -18,26 +17,27 @@ async def test_host_mode_climate_basic_state( api_client_connected: APIClientConnectedFactory, ) -> None: """Test basic climate state reporting.""" - loop = asyncio.get_running_loop() async with run_compiled(yaml_config), api_client_connected() as client: - states: dict[int, EntityState] = {} - climate_future: asyncio.Future[EntityState] = loop.create_future() + # Get entities and set up state synchronization + entities, services = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] + assert len(climate_infos) >= 1, "Expected at least 1 climate entity" - def on_state(state: EntityState) -> None: - states[state.key] = state - if ( - isinstance(state, aioesphomeapi.ClimateState) - and not climate_future.done() - ): - climate_future.set_result(state) - - client.subscribe_states(on_state) + # Subscribe with the wrapper (no-op callback since we just want initial states) + client.subscribe_states(initial_state_helper.on_state_wrapper(lambda _: None)) + # Wait for all initial states to be broadcast try: - climate_state = await asyncio.wait_for(climate_future, timeout=5.0) + await initial_state_helper.wait_for_initial_states() except TimeoutError: - pytest.fail("Climate state not received within 5 seconds") + pytest.fail("Timeout waiting for initial states") + # Get the climate entity and its initial state + test_climate = climate_infos[0] + climate_state = initial_state_helper.initial_states.get(test_climate.key) + + assert climate_state is not None, "Climate initial state not found" assert isinstance(climate_state, aioesphomeapi.ClimateState) assert climate_state.mode == ClimateMode.OFF assert climate_state.action == ClimateAction.OFF From f1bce262ed0f9f0e4eeea306b82ba070dbab59de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Oct 2025 15:48:20 -0500 Subject: [PATCH 180/394] [uart] Optimize UART components to eliminate temporary vector allocations (#11570) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/uart/__init__.py | 2 +- esphome/components/uart/automation.h | 8 ++++++-- esphome/components/uart/button/__init__.py | 2 +- esphome/components/uart/button/uart_button.h | 3 ++- esphome/components/uart/switch/__init__.py | 6 +++--- esphome/components/uart/switch/uart_switch.h | 6 ++++-- 6 files changed, 17 insertions(+), 10 deletions(-) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index f8f927d469..eb911ed007 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -446,7 +446,7 @@ async def uart_write_to_code(config, action_id, template_arg, args): templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_data_template(templ)) else: - cg.add(var.set_data_static(data)) + cg.add(var.set_data_static(cg.ArrayInitializer(*data))) return var diff --git a/esphome/components/uart/automation.h b/esphome/components/uart/automation.h index b6a50ea22d..9c599253de 100644 --- a/esphome/components/uart/automation.h +++ b/esphome/components/uart/automation.h @@ -14,8 +14,12 @@ template class UARTWriteAction : public Action, public Pa this->data_func_ = func; this->static_ = false; } - void set_data_static(const std::vector &data) { - this->data_static_ = data; + void set_data_static(std::vector &&data) { + this->data_static_ = std::move(data); + this->static_ = true; + } + void set_data_static(std::initializer_list data) { + this->data_static_ = std::vector(data); this->static_ = true; } diff --git a/esphome/components/uart/button/__init__.py b/esphome/components/uart/button/__init__.py index 5b811de07d..95fe21271d 100644 --- a/esphome/components/uart/button/__init__.py +++ b/esphome/components/uart/button/__init__.py @@ -33,4 +33,4 @@ async def to_code(config): data = config[CONF_DATA] if isinstance(data, bytes): data = [HexInt(x) for x in data] - cg.add(var.set_data(data)) + cg.add(var.set_data(cg.ArrayInitializer(*data))) diff --git a/esphome/components/uart/button/uart_button.h b/esphome/components/uart/button/uart_button.h index 2d600b199a..8c7d762a05 100644 --- a/esphome/components/uart/button/uart_button.h +++ b/esphome/components/uart/button/uart_button.h @@ -11,7 +11,8 @@ namespace uart { class UARTButton : public button::Button, public UARTDevice, public Component { public: - void set_data(const std::vector &data) { this->data_ = data; } + void set_data(std::vector &&data) { this->data_ = std::move(data); } + void set_data(std::initializer_list data) { this->data_ = std::vector(data); } void dump_config() override; diff --git a/esphome/components/uart/switch/__init__.py b/esphome/components/uart/switch/__init__.py index b25e070461..290bbed5d3 100644 --- a/esphome/components/uart/switch/__init__.py +++ b/esphome/components/uart/switch/__init__.py @@ -44,16 +44,16 @@ async def to_code(config): if data_on := data.get(CONF_TURN_ON): if isinstance(data_on, bytes): data_on = [HexInt(x) for x in data_on] - cg.add(var.set_data_on(data_on)) + cg.add(var.set_data_on(cg.ArrayInitializer(*data_on))) if data_off := data.get(CONF_TURN_OFF): if isinstance(data_off, bytes): data_off = [HexInt(x) for x in data_off] - cg.add(var.set_data_off(data_off)) + cg.add(var.set_data_off(cg.ArrayInitializer(*data_off))) else: data = config[CONF_DATA] if isinstance(data, bytes): data = [HexInt(x) for x in data] - cg.add(var.set_data_on(data)) + cg.add(var.set_data_on(cg.ArrayInitializer(*data))) cg.add(var.set_single_state(True)) if CONF_SEND_EVERY in config: cg.add(var.set_send_every(config[CONF_SEND_EVERY])) diff --git a/esphome/components/uart/switch/uart_switch.h b/esphome/components/uart/switch/uart_switch.h index 4ef5b6da4b..909307d57e 100644 --- a/esphome/components/uart/switch/uart_switch.h +++ b/esphome/components/uart/switch/uart_switch.h @@ -14,8 +14,10 @@ class UARTSwitch : public switch_::Switch, public UARTDevice, public Component { public: void loop() override; - void set_data_on(const std::vector &data) { this->data_on_ = data; } - void set_data_off(const std::vector &data) { this->data_off_ = data; } + void set_data_on(std::vector &&data) { this->data_on_ = std::move(data); } + void set_data_on(std::initializer_list data) { this->data_on_ = std::vector(data); } + void set_data_off(std::vector &&data) { this->data_off_ = std::move(data); } + void set_data_off(std::initializer_list data) { this->data_off_ = std::vector(data); } void set_send_every(uint32_t send_every) { this->send_every_ = send_every; } void set_single_state(bool single) { this->single_state_ = single; } From e46221750048ccd5242d720fe6ce50109a5c7feb Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Tue, 28 Oct 2025 23:18:47 +0100 Subject: [PATCH 181/394] [packages] Tighten package validation (#11584) --- esphome/components/packages/__init__.py | 2 +- .../component_tests/packages/test_packages.py | 46 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index fdc75d995a..04057c07f2 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -102,7 +102,7 @@ CONFIG_SCHEMA = cv.Any( str: PACKAGE_SCHEMA, } ), - cv.ensure_list(PACKAGE_SCHEMA), + [PACKAGE_SCHEMA], ) diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py index d66ca58a69..1c4c91aa52 100644 --- a/tests/component_tests/packages/test_packages.py +++ b/tests/component_tests/packages/test_packages.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch import pytest -from esphome.components.packages import do_packages_pass +from esphome.components.packages import CONFIG_SCHEMA, do_packages_pass from esphome.config import resolve_extend_remove from esphome.config_helpers import Extend, Remove import esphome.config_validation as cv @@ -94,6 +94,50 @@ def test_package_invalid_dict(basic_esphome, basic_wifi): packages_pass(config) +@pytest.mark.parametrize( + "package", + [ + {"package1": "github://esphome/non-existant-repo/file1.yml@main"}, + {"package2": "github://esphome/non-existant-repo/file1.yml"}, + {"package3": "github://esphome/non-existant-repo/other-folder/file1.yml"}, + [ + "github://esphome/non-existant-repo/file1.yml@main", + "github://esphome/non-existant-repo/file1.yml", + "github://esphome/non-existant-repo/other-folder/file1.yml", + ], + ], +) +def test_package_shorthand(package): + CONFIG_SCHEMA(package) + + +@pytest.mark.parametrize( + "package", + [ + # not github + {"package1": "someplace://esphome/non-existant-repo/file1.yml@main"}, + # missing repo + {"package2": "github://esphome/file1.yml"}, + # missing file + {"package3": "github://esphome/non-existant-repo/@main"}, + {"a": "invalid string, not shorthand"}, + "some string", + 3, + False, + {"a": 8}, + ["someplace://esphome/non-existant-repo/file1.yml@main"], + ["github://esphome/file1.yml"], + ["github://esphome/non-existant-repo/@main"], + ["some string"], + [True], + [3], + ], +) +def test_package_invalid(package): + with pytest.raises(cv.Invalid): + CONFIG_SCHEMA(package) + + def test_package_include(basic_wifi, basic_esphome): """ Tests the simple case where an independent config present in a package is added to the top-level config as is. From 466d4522bc05a0274f371ed72e5ea15f3147b148 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:17:16 +1300 Subject: [PATCH 182/394] [http_request] Pass trigger variables into on_response/on_error (#11464) --- esphome/components/http_request/__init__.py | 55 ++++++++++--------- .../components/http_request/http_request.h | 53 +++++++++++------- esphome/core/defines.h | 4 ++ tests/components/http_request/common.yaml | 45 --------------- .../components/http_request/http_request.yaml | 14 +++++ 5 files changed, 79 insertions(+), 92 deletions(-) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index e428838c83..f4fa448c5b 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -12,7 +12,6 @@ from esphome.const import ( CONF_ON_ERROR, CONF_ON_RESPONSE, CONF_TIMEOUT, - CONF_TRIGGER_ID, CONF_URL, CONF_WATCHDOG_TIMEOUT, PLATFORM_HOST, @@ -216,16 +215,8 @@ HTTP_REQUEST_ACTION_SCHEMA = cv.Schema( f"{CONF_VERIFY_SSL} has moved to the base component configuration." ), cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean, - cv.Optional(CONF_ON_RESPONSE): automation.validate_automation( - {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(HttpRequestResponseTrigger)} - ), - cv.Optional(CONF_ON_ERROR): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - automation.Trigger.template() - ) - } - ), + cv.Optional(CONF_ON_RESPONSE): automation.validate_automation(single=True), + cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True), cv.Optional(CONF_MAX_RESPONSE_BUFFER_SIZE, default="1kB"): cv.validate_bytes, } ) @@ -280,7 +271,12 @@ async def http_request_action_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(config[CONF_URL], args, cg.std_string) cg.add(var.set_url(template_)) cg.add(var.set_method(config[CONF_METHOD])) - cg.add(var.set_capture_response(config[CONF_CAPTURE_RESPONSE])) + + capture_response = config[CONF_CAPTURE_RESPONSE] + if capture_response: + cg.add(var.set_capture_response(capture_response)) + cg.add_define("USE_HTTP_REQUEST_RESPONSE") + cg.add(var.set_max_response_buffer_size(config[CONF_MAX_RESPONSE_BUFFER_SIZE])) if CONF_BODY in config: @@ -303,21 +299,26 @@ async def http_request_action_to_code(config, action_id, template_arg, args): for value in config.get(CONF_COLLECT_HEADERS, []): cg.add(var.add_collect_header(value)) - for conf in config.get(CONF_ON_RESPONSE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) - cg.add(var.register_response_trigger(trigger)) - await automation.build_automation( - trigger, - [ - (cg.std_shared_ptr.template(HttpContainer), "response"), - (cg.std_string_ref, "body"), - ], - conf, - ) - for conf in config.get(CONF_ON_ERROR, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) - cg.add(var.register_error_trigger(trigger)) - await automation.build_automation(trigger, [], conf) + if response_conf := config.get(CONF_ON_RESPONSE): + if capture_response: + await automation.build_automation( + var.get_success_trigger_with_response(), + [ + (cg.std_shared_ptr.template(HttpContainer), "response"), + (cg.std_string_ref, "body"), + *args, + ], + response_conf, + ) + else: + await automation.build_automation( + var.get_success_trigger(), + [(cg.std_shared_ptr.template(HttpContainer), "response"), *args], + response_conf, + ) + + if error_conf := config.get(CONF_ON_ERROR): + await automation.build_automation(var.get_error_trigger(), args, error_conf) return var diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 5010cf47a0..482cd2da44 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -183,7 +183,9 @@ template class HttpRequestSendAction : public Action { TEMPLATABLE_VALUE(std::string, url) TEMPLATABLE_VALUE(const char *, method) TEMPLATABLE_VALUE(std::string, body) +#ifdef USE_HTTP_REQUEST_RESPONSE TEMPLATABLE_VALUE(bool, capture_response) +#endif void add_request_header(const char *key, TemplatableValue value) { this->request_headers_.insert({key, value}); @@ -195,9 +197,14 @@ template class HttpRequestSendAction : public Action { void set_json(std::function json_func) { this->json_func_ = json_func; } - void register_response_trigger(HttpRequestResponseTrigger *trigger) { this->response_triggers_.push_back(trigger); } +#ifdef USE_HTTP_REQUEST_RESPONSE + Trigger, std::string &, Ts...> *get_success_trigger_with_response() const { + return this->success_trigger_with_response_; + } +#endif + Trigger, Ts...> *get_success_trigger() const { return this->success_trigger_; } - void register_error_trigger(Trigger<> *trigger) { this->error_triggers_.push_back(trigger); } + Trigger *get_error_trigger() const { return this->error_trigger_; } void set_max_response_buffer_size(size_t max_response_buffer_size) { this->max_response_buffer_size_ = max_response_buffer_size; @@ -228,17 +235,20 @@ template class HttpRequestSendAction : public Action { auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, request_headers, this->collect_headers_); + auto captured_args = std::make_tuple(x...); + if (container == nullptr) { - for (auto *trigger : this->error_triggers_) - trigger->trigger(); + std::apply([this](Ts... captured_args_inner) { this->error_trigger_->trigger(captured_args_inner...); }, + captured_args); return; } size_t content_length = container->content_length; size_t max_length = std::min(content_length, this->max_response_buffer_size_); - std::string response_body; +#ifdef USE_HTTP_REQUEST_RESPONSE if (this->capture_response_.value(x...)) { + std::string response_body; RAMAllocator allocator; uint8_t *buf = allocator.allocate(max_length); if (buf != nullptr) { @@ -253,19 +263,17 @@ template class HttpRequestSendAction : public Action { response_body.assign((char *) buf, read_index); allocator.deallocate(buf, max_length); } - } - - if (this->response_triggers_.size() == 1) { - // if there is only one trigger, no need to copy the response body - this->response_triggers_[0]->process(container, response_body); - } else { - for (auto *trigger : this->response_triggers_) { - // with multiple triggers, pass a copy of the response body to each - // one so that modifications made in one trigger are not visible to - // the others - auto response_body_copy = std::string(response_body); - trigger->process(container, response_body_copy); - } + std::apply( + [this, &container, &response_body](Ts... captured_args_inner) { + this->success_trigger_with_response_->trigger(container, response_body, captured_args_inner...); + }, + captured_args); + } else +#endif + { + std::apply([this, &container]( + Ts... captured_args_inner) { this->success_trigger_->trigger(container, captured_args_inner...); }, + captured_args); } container->end(); } @@ -283,8 +291,13 @@ template class HttpRequestSendAction : public Action { std::set collect_headers_{"content-type", "content-length"}; std::map> json_{}; std::function json_func_{nullptr}; - std::vector response_triggers_{}; - std::vector *> error_triggers_{}; +#ifdef USE_HTTP_REQUEST_RESPONSE + Trigger, std::string &, Ts...> *success_trigger_with_response_ = + new Trigger, std::string &, Ts...>(); +#endif + Trigger, Ts...> *success_trigger_ = + new Trigger, Ts...>(); + Trigger *error_trigger_ = new Trigger(); size_t max_response_buffer_size_{SIZE_MAX}; }; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 97e766455a..868df6e254 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -187,6 +187,7 @@ #define ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT 1 #define ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT 2 #define USE_ESP32_CAMERA_JPEG_ENCODER +#define USE_HTTP_REQUEST_RESPONSE #define USE_I2C #define USE_IMPROV #define USE_ESP32_IMPROV_NEXT_URL @@ -237,6 +238,7 @@ #define USE_CAPTIVE_PORTAL #define USE_ESP8266_PREFERENCES_FLASH #define USE_HTTP_REQUEST_ESP8266_HTTPS +#define USE_HTTP_REQUEST_RESPONSE #define USE_I2C #define USE_SOCKET_IMPL_LWIP_TCP @@ -257,6 +259,7 @@ #ifdef USE_RP2040 #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 0) +#define USE_HTTP_REQUEST_RESPONSE #define USE_I2C #define USE_LOGGER_USB_CDC #define USE_SOCKET_IMPL_LWIP_TCP @@ -273,6 +276,7 @@ #endif #ifdef USE_HOST +#define USE_HTTP_REQUEST_RESPONSE #define USE_SOCKET_IMPL_BSD_SOCKETS #define USE_SOCKET_SELECT_SUPPORT #endif diff --git a/tests/components/http_request/common.yaml b/tests/components/http_request/common.yaml index 9ff9f9fb67..62d0a7941a 100644 --- a/tests/components/http_request/common.yaml +++ b/tests/components/http_request/common.yaml @@ -4,51 +4,6 @@ wifi: ssid: MySSID password: password1 -esphome: - on_boot: - then: - - http_request.get: - url: https://esphome.io - request_headers: - Content-Type: application/json - collect_headers: - - age - on_error: - logger.log: "Request failed" - on_response: - then: - - logger.log: - format: "Response status: %d, Duration: %lu ms, age: %s" - args: - - response->status_code - - (long) response->duration_ms - - response->get_response_header("age").c_str() - - http_request.post: - url: https://esphome.io - request_headers: - Content-Type: application/json - json: - key: value - - http_request.send: - method: PUT - url: https://esphome.io - request_headers: - Content-Type: application/json - body: "Some data" - -http_request: - useragent: esphome/tagreader - timeout: 10s - verify_ssl: ${verify_ssl} - -script: - - id: does_not_compile - parameters: - api_url: string - then: - - http_request.get: - url: "http://google.com" - ota: - platform: http_request id: http_request_ota diff --git a/tests/components/http_request/http_request.yaml b/tests/components/http_request/http_request.yaml index ea7f6bf5a7..13ca5ceba0 100644 --- a/tests/components/http_request/http_request.yaml +++ b/tests/components/http_request/http_request.yaml @@ -31,6 +31,20 @@ esphome: request_headers: Content-Type: application/json body: "Some data" + - http_request.post: + url: https://esphome.io + request_headers: + Content-Type: application/json + json: + key: value + capture_response: true + on_response: + then: + - logger.log: + format: "Captured response status: %d, Body: %s" + args: + - response->status_code + - body.c_str() http_request: useragent: esphome/tagreader From 78d780105bf9a9a25a5b3f570f178427f4d2c783 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Oct 2025 19:24:37 -0500 Subject: [PATCH 183/394] [ci] Change upper Python version being tested to 3.13 (#11587) --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb04f6bf8d..655e28e3b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,7 +114,7 @@ jobs: matrix: python-version: - "3.11" - - "3.14" + - "3.13" os: - ubuntu-latest - macOS-latest @@ -123,9 +123,9 @@ jobs: # Minimize CI resource usage # by only running the Python version # version used for docker images on Windows and macOS - - python-version: "3.14" + - python-version: "3.13" os: windows-latest - - python-version: "3.14" + - python-version: "3.13" os: macOS-latest runs-on: ${{ matrix.os }} needs: From 249cd7415badfc720894e4dd9a64d0c4625428bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 00:32:41 +0000 Subject: [PATCH 184/394] Bump aioesphomeapi from 42.3.0 to 42.4.0 (#11586) 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 4a64bd39cc..b0d7d62c36 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.1.0 click==8.1.7 esphome-dashboard==20251013.0 -aioesphomeapi==42.3.0 +aioesphomeapi==42.4.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.18.16 # dashboard_import From 4f2d54be4edafbe87996c20529b938f8eef8e93b Mon Sep 17 00:00:00 2001 From: Kent Gibson Date: Wed, 29 Oct 2025 08:48:26 +0800 Subject: [PATCH 185/394] template_alarm_control_panel cleanups (#11469) --- .../template_alarm_control_panel.cpp | 68 ++++++++----------- .../template_alarm_control_panel.h | 8 +-- 2 files changed, 33 insertions(+), 43 deletions(-) diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index eac0629480..d1562ee82f 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -80,19 +80,12 @@ void TemplateAlarmControlPanel::dump_config() { } void TemplateAlarmControlPanel::setup() { - switch (this->restore_mode_) { - case ALARM_CONTROL_PANEL_ALWAYS_DISARMED: - this->current_state_ = ACP_STATE_DISARMED; - break; - case ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED: { - uint8_t value; - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); - if (this->pref_.load(&value)) { - this->current_state_ = static_cast(value); - } else { - this->current_state_ = ACP_STATE_DISARMED; - } - break; + this->current_state_ = ACP_STATE_DISARMED; + if (this->restore_mode_ == ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED) { + uint8_t value; + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + if (this->pref_.load(&value)) { + this->current_state_ = static_cast(value); } } this->desired_state_ = this->current_state_; @@ -119,15 +112,15 @@ void TemplateAlarmControlPanel::loop() { this->publish_state(ACP_STATE_TRIGGERED); return; } - auto future_state = this->current_state_; + auto next_state = this->current_state_; // reset triggered if all clear if (this->current_state_ == ACP_STATE_TRIGGERED && this->trigger_time_ > 0 && (millis() - this->last_update_) > this->trigger_time_) { - future_state = this->desired_state_; + next_state = this->desired_state_; } - bool delayed_sensor_not_ready = false; - bool instant_sensor_not_ready = false; + bool delayed_sensor_faulted = false; + bool instant_sensor_faulted = false; #ifdef USE_BINARY_SENSOR // Test all of the sensors in the list regardless of the alarm panel state @@ -144,7 +137,7 @@ void TemplateAlarmControlPanel::loop() { // Record the sensor state change this->sensor_data_[sensor_info.second.store_index].last_chime_state = sensor_info.first->state; } - // Check for triggered sensors + // Check for faulted sensors if (sensor_info.first->state) { // Sensor triggered? // Skip if auto bypassed if (std::count(this->bypassed_sensor_indicies_.begin(), this->bypassed_sensor_indicies_.end(), @@ -163,42 +156,41 @@ void TemplateAlarmControlPanel::loop() { } switch (sensor_info.second.type) { - case ALARM_SENSOR_TYPE_INSTANT: - instant_sensor_not_ready = true; - break; case ALARM_SENSOR_TYPE_INSTANT_ALWAYS: - instant_sensor_not_ready = true; - future_state = ACP_STATE_TRIGGERED; + next_state = ACP_STATE_TRIGGERED; + [[fallthrough]]; + case ALARM_SENSOR_TYPE_INSTANT: + instant_sensor_faulted = true; break; case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER: // Look to see if we are in the pending state if (this->current_state_ == ACP_STATE_PENDING) { - delayed_sensor_not_ready = true; + delayed_sensor_faulted = true; } else { - instant_sensor_not_ready = true; + instant_sensor_faulted = true; } break; case ALARM_SENSOR_TYPE_DELAYED: default: - delayed_sensor_not_ready = true; + delayed_sensor_faulted = true; } } } - // Update all sensors not ready flag - this->sensors_ready_ = ((!instant_sensor_not_ready) && (!delayed_sensor_not_ready)); + // Update all sensors ready flag + bool sensors_ready = !(instant_sensor_faulted || delayed_sensor_faulted); // Call the ready state change callback if there was a change - if (this->sensors_ready_ != this->sensors_ready_last_) { + if (this->sensors_ready_ != sensors_ready) { + this->sensors_ready_ = sensors_ready; this->ready_callback_.call(); - this->sensors_ready_last_ = this->sensors_ready_; } #endif - if (this->is_state_armed(future_state) && (!this->sensors_ready_)) { + if (this->is_state_armed(next_state) && (!this->sensors_ready_)) { // Instant sensors - if (instant_sensor_not_ready) { + if (instant_sensor_faulted) { this->publish_state(ACP_STATE_TRIGGERED); - } else if (delayed_sensor_not_ready) { + } else if (delayed_sensor_faulted) { // Delayed sensors if ((this->pending_time_ > 0) && (this->current_state_ != ACP_STATE_TRIGGERED)) { this->publish_state(ACP_STATE_PENDING); @@ -206,8 +198,8 @@ void TemplateAlarmControlPanel::loop() { this->publish_state(ACP_STATE_TRIGGERED); } } - } else if (future_state != this->current_state_) { - this->publish_state(future_state); + } else if (next_state != this->current_state_) { + this->publish_state(next_state); } } @@ -234,8 +226,6 @@ uint32_t TemplateAlarmControlPanel::get_supported_features() const { return features; } -bool TemplateAlarmControlPanel::get_requires_code() const { return !this->codes_.empty(); } - void TemplateAlarmControlPanel::arm_(optional code, alarm_control_panel::AlarmControlPanelState state, uint32_t delay) { if (this->current_state_ != ACP_STATE_DISARMED) { @@ -258,9 +248,9 @@ void TemplateAlarmControlPanel::arm_(optional code, alarm_control_p void TemplateAlarmControlPanel::bypass_before_arming() { #ifdef USE_BINARY_SENSOR for (auto sensor_info : this->sensor_map_) { - // Check for sensors left on and set to bypass automatically and remove them from monitoring + // Check for faulted bypass_auto sensors and remove them from monitoring if ((sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor_info.first->state)) { - ESP_LOGW(TAG, "'%s' is left on and will be automatically bypassed", sensor_info.first->get_name().c_str()); + ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", sensor_info.first->get_name().c_str()); this->bypassed_sensor_indicies_.push_back(sensor_info.second.store_index); } } diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h index c3b28e8efa..40a79004da 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h @@ -56,7 +56,7 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, void setup() override; void loop() override; uint32_t get_supported_features() const override; - bool get_requires_code() const override; + bool get_requires_code() const override { return !this->codes_.empty(); } bool get_requires_code_to_arm() const override { return this->requires_code_to_arm_; } bool get_all_sensors_ready() { return this->sensors_ready_; }; void set_restore_mode(TemplateAlarmControlPanelRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } @@ -66,7 +66,8 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, /** Add a binary_sensor to the alarm_panel. * * @param sensor The BinarySensor instance. - * @param ignore_when_home if this should be ignored when armed_home mode + * @param flags The OR of BinarySensorFlags for the sensor. + * @param type The sensor type which determines its triggering behaviour. */ void add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags = 0, AlarmSensorType type = ALARM_SENSOR_TYPE_DELAYED); @@ -121,7 +122,7 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, protected: void control(const alarm_control_panel::AlarmControlPanelCall &call) override; #ifdef USE_BINARY_SENSOR - // This maps a binary sensor to its type and attribute bits + // This maps a binary sensor to its alarm specific info std::map sensor_map_; // a list of automatically bypassed sensors std::vector bypassed_sensor_indicies_; @@ -147,7 +148,6 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, bool supports_arm_home_ = false; bool supports_arm_night_ = false; bool sensors_ready_ = false; - bool sensors_ready_last_ = false; uint8_t next_store_index_ = 0; // check if the code is valid bool is_code_valid_(optional code); From 25e4aafd7146a43883213eede281193ce75745b8 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 29 Oct 2025 14:28:29 +1300 Subject: [PATCH 186/394] [ci] Fix auto labeller workflow with wrong comment for too-big with labels (#11592) --- .github/workflows/auto-label-pr.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 4e2f086f47..dd1bc29d83 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -416,7 +416,7 @@ jobs: } // Generate review messages - function generateReviewMessages(finalLabels) { + function generateReviewMessages(finalLabels, originalLabelCount) { const messages = []; const prAuthor = context.payload.pull_request.user.login; @@ -430,15 +430,15 @@ jobs: .reduce((sum, file) => sum + (file.deletions || 0), 0); const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions); - const tooManyLabels = finalLabels.length > MAX_LABELS; + const tooManyLabels = originalLabelCount > MAX_LABELS; const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD; let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`; if (tooManyLabels && tooManyChanges) { - message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${finalLabels.length} different components/areas.`; + message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLabelCount} different components/areas.`; } else if (tooManyLabels) { - message += `This PR affects ${finalLabels.length} different components/areas.`; + message += `This PR affects ${originalLabelCount} different components/areas.`; } else { message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`; } @@ -466,8 +466,8 @@ jobs: } // Handle reviews - async function handleReviews(finalLabels) { - const reviewMessages = generateReviewMessages(finalLabels); + async function handleReviews(finalLabels, originalLabelCount) { + const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount); const hasReviewableLabels = finalLabels.some(label => ['too-big', 'needs-codeowners'].includes(label) ); @@ -627,6 +627,7 @@ jobs: // Handle too many labels (only for non-mega PRs) const tooManyLabels = finalLabels.length > MAX_LABELS; + const originalLabelCount = finalLabels.length; if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) { finalLabels = ['too-big']; @@ -635,7 +636,7 @@ jobs: console.log('Computed labels:', finalLabels.join(', ')); // Handle reviews - await handleReviews(finalLabels); + await handleReviews(finalLabels, originalLabelCount); // Apply labels if (finalLabels.length > 0) { From 99f48ae51c79d0159188d679f5b8659be488c7af Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 29 Oct 2025 14:29:40 +1300 Subject: [PATCH 187/394] [logger] Improve level validation errors (#11589) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/logger/__init__.py | 35 +++++++++++++++++++++------ 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 22bf3d2f4c..cf78e6ae63 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -173,14 +173,34 @@ def uart_selection(value): raise NotImplementedError -def validate_local_no_higher_than_global(value): - global_level = LOG_LEVEL_SEVERITY.index(value[CONF_LEVEL]) - for tag, level in value.get(CONF_LOGS, {}).items(): - if LOG_LEVEL_SEVERITY.index(level) > global_level: - raise cv.Invalid( - f"The configured log level for {tag} ({level}) must be no more severe than the global log level {value[CONF_LEVEL]}." +def validate_local_no_higher_than_global(config): + global_level = config[CONF_LEVEL] + global_level_index = LOG_LEVEL_SEVERITY.index(global_level) + errs = [] + for tag, level in config.get(CONF_LOGS, {}).items(): + if LOG_LEVEL_SEVERITY.index(level) > global_level_index: + errs.append( + cv.Invalid( + f"The configured log level for {tag} ({level}) must not be less severe than the global log level ({global_level})", + [CONF_LOGS, tag], + ) ) - return value + if errs: + raise cv.MultipleInvalid(errs) + return config + + +def validate_initial_no_higher_than_global(config): + if initial_level := config.get(CONF_INITIAL_LEVEL): + global_level = config[CONF_LEVEL] + if LOG_LEVEL_SEVERITY.index(initial_level) > LOG_LEVEL_SEVERITY.index( + global_level + ): + raise cv.Invalid( + f"The initial log level ({initial_level}) must not be less severe than the global log level ({global_level})", + [CONF_INITIAL_LEVEL], + ) + return config Logger = logger_ns.class_("Logger", cg.Component) @@ -263,6 +283,7 @@ CONFIG_SCHEMA = cv.All( } ).extend(cv.COMPONENT_SCHEMA), validate_local_no_higher_than_global, + validate_initial_no_higher_than_global, ) From 0d805355f5bc9753dc7cfe3e9bcd844424aeb746 Mon Sep 17 00:00:00 2001 From: Anton Sergunov Date: Wed, 29 Oct 2025 07:33:16 +0600 Subject: [PATCH 188/394] Fix the LiberTiny bug with UART pin setup (#11518) --- .../uart/uart_component_libretiny.cpp | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index 9c065fe5df..8d1d28fce4 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -46,40 +46,58 @@ uint16_t LibreTinyUARTComponent::get_config() { } void LibreTinyUARTComponent::setup() { - if (this->rx_pin_) { - this->rx_pin_->setup(); - } - if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { - this->tx_pin_->setup(); - } - int8_t tx_pin = tx_pin_ == nullptr ? -1 : tx_pin_->get_pin(); int8_t rx_pin = rx_pin_ == nullptr ? -1 : rx_pin_->get_pin(); bool tx_inverted = tx_pin_ != nullptr && tx_pin_->is_inverted(); bool rx_inverted = rx_pin_ != nullptr && rx_pin_->is_inverted(); + auto shouldFallbackToSoftwareSerial = [&]() -> bool { + auto hasFlags = [](InternalGPIOPin *pin, const gpio::Flags mask) -> bool { + 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)) { +#if LT_ARD_HAS_SOFTSERIAL + ESP_LOGI(TAG, "Pins has flags set. Using Software Serial"); + return true; +#else + ESP_LOGW(TAG, "Pin flags are set but not supported for hardware serial. Ignoring"); +#endif + } + return false; + }; + if (false) return; #if LT_HW_UART0 - else if ((tx_pin == -1 || tx_pin == PIN_SERIAL0_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL0_RX)) { + else if ((tx_pin == -1 || tx_pin == PIN_SERIAL0_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL0_RX) && + !shouldFallbackToSoftwareSerial()) { this->serial_ = &Serial0; this->hardware_idx_ = 0; } #endif #if LT_HW_UART1 - else if ((tx_pin == -1 || tx_pin == PIN_SERIAL1_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL1_RX)) { + else if ((tx_pin == -1 || tx_pin == PIN_SERIAL1_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL1_RX) && + !shouldFallbackToSoftwareSerial()) { this->serial_ = &Serial1; this->hardware_idx_ = 1; } #endif #if LT_HW_UART2 - else if ((tx_pin == -1 || tx_pin == PIN_SERIAL2_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL2_RX)) { + else if ((tx_pin == -1 || tx_pin == PIN_SERIAL2_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL2_RX) && + !shouldFallbackToSoftwareSerial()) { this->serial_ = &Serial2; this->hardware_idx_ = 2; } #endif else { #if LT_ARD_HAS_SOFTSERIAL + if (this->rx_pin_) { + this->rx_pin_->setup(); + } + if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { + this->tx_pin_->setup(); + } this->serial_ = new SoftwareSerial(rx_pin, tx_pin, rx_inverted || tx_inverted); #else this->serial_ = &Serial; From 5528c3c765f434b061df0a08269886de6e8ba2d6 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 29 Oct 2025 11:37:14 +1000 Subject: [PATCH 189/394] [mipi_rgb] Fix rotation with custom model (#11585) --- esphome/components/mipi/__init__.py | 12 ++++++++ esphome/components/mipi_rgb/display.py | 38 ++++++++++++++------------ esphome/components/mipi_spi/display.py | 15 +--------- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/esphome/components/mipi/__init__.py b/esphome/components/mipi/__init__.py index 4dff1af62a..93d1750cd6 100644 --- a/esphome/components/mipi/__init__.py +++ b/esphome/components/mipi/__init__.py @@ -384,6 +384,18 @@ class DriverChip: transform[CONF_TRANSFORM] = True return transform + def swap_xy_schema(self): + uses_swap = self.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED + + def validator(value): + if value: + raise cv.Invalid("Axis swapping not supported by this model") + return cv.boolean(value) + + if uses_swap: + return {cv.Required(CONF_SWAP_XY): cv.boolean} + return {cv.Optional(CONF_SWAP_XY, default=False): validator} + def add_madctl(self, sequence: list, config: dict): # Add the MADCTL command to the sequence based on the configuration. use_flip = config.get(CONF_USE_AXIS_FLIPS) diff --git a/esphome/components/mipi_rgb/display.py b/esphome/components/mipi_rgb/display.py index 3001d33980..9d6b1fa729 100644 --- a/esphome/components/mipi_rgb/display.py +++ b/esphome/components/mipi_rgb/display.py @@ -46,6 +46,7 @@ from esphome.const import ( CONF_DATA_RATE, CONF_DC_PIN, CONF_DIMENSIONS, + CONF_DISABLED, CONF_ENABLE_PIN, CONF_GREEN, CONF_HSYNC_PIN, @@ -117,16 +118,16 @@ def data_pin_set(length): def model_schema(config): model = MODELS[config[CONF_MODEL].upper()] - if transforms := model.transforms: - transform = cv.Schema({cv.Required(x): cv.boolean for x in transforms}) - for x in (CONF_SWAP_XY, CONF_MIRROR_X, CONF_MIRROR_Y): - if x not in transforms: - transform = transform.extend( - {cv.Optional(x): cv.invalid(f"{x} not supported by this model")} - ) - else: - transform = cv.invalid("This model does not support transforms") - + transform = cv.Any( + cv.Schema( + { + cv.Required(CONF_MIRROR_X): cv.boolean, + cv.Required(CONF_MIRROR_Y): cv.boolean, + **model.swap_xy_schema(), + } + ), + cv.one_of(CONF_DISABLED, lower=True), + ) # RPI model does not use an init sequence, indicates with empty list if model.initsequence is None: # Custom model requires an init sequence @@ -135,12 +136,16 @@ def model_schema(config): else: iseqconf = cv.Optional(CONF_INIT_SEQUENCE) uses_spi = CONF_INIT_SEQUENCE in config or len(model.initsequence) != 0 - swap_xy = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY, False) - - # Dimensions are optional if the model has a default width and the swap_xy transform is not overridden - cv_dimensions = ( - cv.Optional if model.get_default(CONF_WIDTH) and not swap_xy else cv.Required + # Dimensions are optional if the model has a default width and the x-y transform is not overridden + transform_config = config.get(CONF_TRANSFORM, {}) + is_swapped = ( + isinstance(transform_config, dict) + and transform_config.get(CONF_SWAP_XY, False) is True ) + cv_dimensions = ( + cv.Optional if model.get_default(CONF_WIDTH) and not is_swapped else cv.Required + ) + pixel_modes = (PIXEL_MODE_16BIT, PIXEL_MODE_18BIT, "16", "18") schema = display.FULL_DISPLAY_SCHEMA.extend( { @@ -157,7 +162,7 @@ def model_schema(config): model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.one_of( *pixel_modes, lower=True ), - model.option(CONF_TRANSFORM, cv.UNDEFINED): transform, + cv.Optional(CONF_TRANSFORM): transform, cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), model.option(CONF_INVERT_COLORS, False): cv.boolean, model.option(CONF_USE_AXIS_FLIPS, True): cv.boolean, @@ -270,7 +275,6 @@ async def to_code(config): cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH])) cg.add(var.set_pclk_inverted(config[CONF_PCLK_INVERTED])) cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY])) - index = 0 dpins = [] if CONF_RED in config[CONF_DATA_PINS]: red_pins = config[CONF_DATA_PINS][CONF_RED] diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index 891c8b42ff..50ea826eab 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -131,19 +131,6 @@ def denominator(config): ) from StopIteration -def swap_xy_schema(model): - uses_swap = model.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED - - def validator(value): - if value: - raise cv.Invalid("Axis swapping not supported by this model") - return cv.boolean(value) - - if uses_swap: - return {cv.Required(CONF_SWAP_XY): cv.boolean} - return {cv.Optional(CONF_SWAP_XY, default=False): validator} - - def model_schema(config): model = MODELS[config[CONF_MODEL]] bus_mode = config[CONF_BUS_MODE] @@ -152,7 +139,7 @@ def model_schema(config): { cv.Required(CONF_MIRROR_X): cv.boolean, cv.Required(CONF_MIRROR_Y): cv.boolean, - **swap_xy_schema(model), + **model.swap_xy_schema(), } ), cv.one_of(CONF_DISABLED, lower=True), From a609343cb665bbb2411204e795ace686b70168c8 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:06:30 +1300 Subject: [PATCH 190/394] [fan] Remove deprecated `set_speed` function (#11590) --- esphome/components/fan/fan.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index b74187eb4a..3739de29a2 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -60,8 +60,6 @@ class FanCall { this->speed_ = speed; return *this; } - ESPDEPRECATED("set_speed() with string argument is deprecated, use integer argument instead.", "2021.9") - FanCall &set_speed(const char *legacy_speed); optional get_speed() const { return this->speed_; } FanCall &set_direction(FanDirection direction) { this->direction_ = direction; From f3634edc22a9c9e939c5bede697bad5aa89fef4d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Oct 2025 21:28:16 -0500 Subject: [PATCH 191/394] [select] Store options in flash to reduce RAM usage (#11514) --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_pb2.cpp | 8 +-- esphome/components/api/api_pb2.h | 2 +- esphome/components/api/api_pb2_dump.cpp | 6 +++ esphome/components/lvgl/lvgl_esphome.h | 2 +- esphome/components/lvgl/select/lvgl_select.h | 12 ++++- .../select/modbus_select.cpp | 12 +++-- esphome/components/select/select.cpp | 7 ++- esphome/components/select/select.h | 3 ++ esphome/components/select/select_traits.cpp | 11 +++- esphome/components/select/select_traits.h | 11 ++-- .../template/select/template_select.cpp | 9 ++-- .../components/tuya/select/tuya_select.cpp | 5 +- esphome/core/helpers.h | 10 ++++ script/api_protobuf/api_protobuf.py | 51 +++++++++++++++---- 15 files changed, 111 insertions(+), 40 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index fae0f2e75a..f50944ffa4 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1143,7 +1143,7 @@ message ListEntitiesSelectResponse { reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; - repeated string options = 6 [(container_pointer) = "std::vector"]; + repeated string options = 6 [(container_pointer_no_template) = "FixedVector"]; bool disabled_by_default = 7; EntityCategory entity_category = 8; uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 37bcf5d8a0..3472707d3c 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1475,8 +1475,8 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon_ref_); #endif - for (const auto &it : *this->options) { - buffer.encode_string(6, it, true); + for (const char *it : *this->options) { + buffer.encode_string(6, it, strlen(it), true); } buffer.encode_bool(7, this->disabled_by_default); buffer.encode_uint32(8, static_cast(this->entity_category)); @@ -1492,8 +1492,8 @@ void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const { size.add_length(1, this->icon_ref_.size()); #endif if (!this->options->empty()) { - for (const auto &it : *this->options) { - size.add_length_force(1, it.size()); + for (const char *it : *this->options) { + size.add_length_force(1, strlen(it)); } } size.add_bool(1, this->disabled_by_default); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 3e9a10c1f7..aa5c031ac7 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1534,7 +1534,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_select_response"; } #endif - const std::vector *options{}; + const FixedVector *options{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index e803125f53..d94ceaaa9c 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -88,6 +88,12 @@ static void dump_field(std::string &out, const char *field_name, StringRef value out.append("\n"); } +static void dump_field(std::string &out, const char *field_name, const char *value, int indent = 2) { + append_field_prefix(out, field_name, indent); + out.append("'").append(value).append("'"); + out.append("\n"); +} + template static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) { append_field_prefix(out, field_name, indent); out.append(proto_enum_to_string(value)); diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 3ae67e8a0b..d3dc8fac5a 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -358,7 +358,7 @@ class LvSelectable : public LvCompound { virtual void set_selected_index(size_t index, lv_anim_enable_t anim) = 0; void set_selected_text(const std::string &text, lv_anim_enable_t anim); std::string get_selected_text(); - std::vector get_options() { return this->options_; } + const std::vector &get_options() { return this->options_; } void set_options(std::vector options); protected: diff --git a/esphome/components/lvgl/select/lvgl_select.h b/esphome/components/lvgl/select/lvgl_select.h index a0e60295a6..3b1fd67d68 100644 --- a/esphome/components/lvgl/select/lvgl_select.h +++ b/esphome/components/lvgl/select/lvgl_select.h @@ -53,7 +53,17 @@ class LVGLSelect : public select::Select, public Component { this->widget_->set_selected_text(value, this->anim_); this->publish(); } - void set_options_() { this->traits.set_options(this->widget_->get_options()); } + void set_options_() { + // Widget uses std::vector, SelectTraits uses FixedVector + // Convert by extracting c_str() pointers + const auto &opts = this->widget_->get_options(); + FixedVector opt_ptrs; + opt_ptrs.init(opts.size()); + for (size_t i = 0; i < opts.size(); i++) { + opt_ptrs[i] = opts[i].c_str(); + } + this->traits.set_options(opt_ptrs); + } LvSelectable *widget_; lv_anim_enable_t anim_; diff --git a/esphome/components/modbus_controller/select/modbus_select.cpp b/esphome/components/modbus_controller/select/modbus_select.cpp index 56b8c783ed..48bf2835f2 100644 --- a/esphome/components/modbus_controller/select/modbus_select.cpp +++ b/esphome/components/modbus_controller/select/modbus_select.cpp @@ -28,7 +28,7 @@ void ModbusSelect::parse_and_publish(const std::vector &data) { if (map_it != this->mapping_.cend()) { size_t idx = std::distance(this->mapping_.cbegin(), map_it); - new_state = this->traits.get_options()[idx]; + new_state = std::string(this->option_at(idx)); ESP_LOGV(TAG, "Found option %s for value %lld", new_state->c_str(), value); } else { ESP_LOGE(TAG, "No option found for mapping %lld", value); @@ -41,10 +41,12 @@ void ModbusSelect::parse_and_publish(const std::vector &data) { } void ModbusSelect::control(const std::string &value) { - auto options = this->traits.get_options(); - auto opt_it = std::find(options.cbegin(), options.cend(), value); - size_t idx = std::distance(options.cbegin(), opt_it); - optional mapval = this->mapping_[idx]; + auto idx = this->index_of(value); + if (!idx.has_value()) { + ESP_LOGW(TAG, "Invalid option '%s'", value.c_str()); + return; + } + optional mapval = this->mapping_[idx.value()]; ESP_LOGD(TAG, "Found value %lld for option '%s'", *mapval, value.c_str()); std::vector data; diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 16e8288ca1..5e30be3c13 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -1,5 +1,6 @@ #include "select.h" #include "esphome/core/log.h" +#include namespace esphome { namespace select { @@ -35,7 +36,7 @@ size_t Select::size() const { optional Select::index_of(const std::string &option) const { const auto &options = traits.get_options(); for (size_t i = 0; i < options.size(); i++) { - if (options[i] == option) { + if (strcmp(options[i], option.c_str()) == 0) { return i; } } @@ -53,11 +54,13 @@ optional Select::active_index() const { optional Select::at(size_t index) const { if (this->has_index(index)) { const auto &options = traits.get_options(); - return options.at(index); + return std::string(options.at(index)); } else { return {}; } } +const char *Select::option_at(size_t index) const { return traits.get_options().at(index); } + } // namespace select } // namespace esphome diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 902b8a78ce..eabb39898b 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -56,6 +56,9 @@ class Select : public EntityBase { /// Return the (optional) option value at the provided index offset. optional at(size_t index) const; + /// Return the option value at the provided index offset (as const char* from flash). + const char *option_at(size_t index) const; + void add_on_state_callback(std::function &&callback); protected: diff --git a/esphome/components/select/select_traits.cpp b/esphome/components/select/select_traits.cpp index a8cd4290c8..c6ded98ebf 100644 --- a/esphome/components/select/select_traits.cpp +++ b/esphome/components/select/select_traits.cpp @@ -3,9 +3,16 @@ namespace esphome { namespace select { -void SelectTraits::set_options(std::vector options) { this->options_ = std::move(options); } +void SelectTraits::set_options(const std::initializer_list &options) { this->options_ = options; } -const std::vector &SelectTraits::get_options() const { return this->options_; } +void SelectTraits::set_options(const FixedVector &options) { + this->options_.init(options.size()); + for (size_t i = 0; i < options.size(); i++) { + this->options_[i] = options[i]; + } +} + +const FixedVector &SelectTraits::get_options() const { return this->options_; } } // namespace select } // namespace esphome diff --git a/esphome/components/select/select_traits.h b/esphome/components/select/select_traits.h index 128066dd6b..ee59a030ad 100644 --- a/esphome/components/select/select_traits.h +++ b/esphome/components/select/select_traits.h @@ -1,18 +1,19 @@ #pragma once -#include -#include +#include "esphome/core/helpers.h" +#include namespace esphome { namespace select { class SelectTraits { public: - void set_options(std::vector options); - const std::vector &get_options() const; + void set_options(const std::initializer_list &options); + void set_options(const FixedVector &options); + const FixedVector &get_options() const; protected: - std::vector options_; + FixedVector options_; }; } // namespace select diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 3765cf02bf..c7a1d8a344 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -16,12 +16,12 @@ void TemplateSelect::setup() { size_t restored_index; if (this->pref_.load(&restored_index) && this->has_index(restored_index)) { index = restored_index; - ESP_LOGD(TAG, "State from restore: %s", this->at(index).value().c_str()); + ESP_LOGD(TAG, "State from restore: %s", this->option_at(index)); } else { - ESP_LOGD(TAG, "State from initial (could not load or invalid stored index): %s", this->at(index).value().c_str()); + ESP_LOGD(TAG, "State from initial (could not load or invalid stored index): %s", this->option_at(index)); } } else { - ESP_LOGD(TAG, "State from initial: %s", this->at(index).value().c_str()); + ESP_LOGD(TAG, "State from initial: %s", this->option_at(index)); } this->publish_state(this->at(index).value()); @@ -64,8 +64,7 @@ void TemplateSelect::dump_config() { " Optimistic: %s\n" " Initial Option: %s\n" " Restore Value: %s", - YESNO(this->optimistic_), this->at(this->initial_option_index_).value().c_str(), - YESNO(this->restore_value_)); + YESNO(this->optimistic_), this->option_at(this->initial_option_index_), YESNO(this->restore_value_)); } } // namespace template_ diff --git a/esphome/components/tuya/select/tuya_select.cpp b/esphome/components/tuya/select/tuya_select.cpp index 91ddbc77ec..7c1cd09d06 100644 --- a/esphome/components/tuya/select/tuya_select.cpp +++ b/esphome/components/tuya/select/tuya_select.cpp @@ -10,7 +10,6 @@ void TuyaSelect::setup() { this->parent_->register_listener(this->select_id_, [this](const TuyaDatapoint &datapoint) { uint8_t enum_value = datapoint.value_enum; ESP_LOGV(TAG, "MCU reported select %u value %u", this->select_id_, enum_value); - auto options = this->traits.get_options(); auto mappings = this->mappings_; auto it = std::find(mappings.cbegin(), mappings.cend(), enum_value); if (it == mappings.end()) { @@ -49,9 +48,9 @@ void TuyaSelect::dump_config() { " Data type: %s\n" " Options are:", this->select_id_, this->is_int_ ? "int" : "enum"); - auto options = this->traits.get_options(); + const auto &options = this->traits.get_options(); for (size_t i = 0; i < this->mappings_.size(); i++) { - ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options.at(i).c_str()); + ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options.at(i)); } } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 9b0591c9c5..cf21ddc16d 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -304,6 +304,11 @@ template class FixedVector { return data_[size_ - 1]; } + /// Access first element (no bounds checking - matches std::vector behavior) + /// Caller must ensure vector is not empty (size() > 0) + T &front() { return data_[0]; } + const T &front() const { return data_[0]; } + /// Access last element (no bounds checking - matches std::vector behavior) /// Caller must ensure vector is not empty (size() > 0) T &back() { return data_[size_ - 1]; } @@ -317,6 +322,11 @@ template class FixedVector { T &operator[](size_t i) { return data_[i]; } const T &operator[](size_t i) const { return data_[i]; } + /// Access element with bounds checking (matches std::vector behavior) + /// Note: No exception thrown on out of bounds - caller must ensure index is valid + T &at(size_t i) { return data_[i]; } + const T &at(size_t i) const { return data_[i]; } + // Iterator support for range-based for loops T *begin() { return data_; } T *end() { return data_ + size_; } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 2f83b0bd79..394e92b9a7 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1162,7 +1162,11 @@ class SInt64Type(TypeInfo): def _generate_array_dump_content( - ti, field_name: str, name: str, is_bool: bool = False + ti, + field_name: str, + name: str, + is_bool: bool = False, + is_const_char_ptr: bool = False, ) -> str: """Generate dump content for array types (repeated or fixed array). @@ -1170,7 +1174,10 @@ def _generate_array_dump_content( """ o = f"for (const auto {'' if is_bool else '&'}it : {field_name}) {{\n" # Check if underlying type can use dump_field - if ti.can_use_dump_field(): + if is_const_char_ptr: + # Special case for const char* - use it directly + o += f' dump_field(out, "{name}", it, 4);\n' + elif ti.can_use_dump_field(): # For types that have dump_field overloads, use them with extra indent # std::vector iterators return proxy objects, need explicit cast value_expr = "static_cast(it)" if is_bool else ti.dump_field_value("it") @@ -1533,11 +1540,16 @@ class RepeatedTypeInfo(TypeInfo): def encode_content(self) -> str: if self._use_pointer: # For pointer fields, just dereference (pointer should never be null in our use case) - o = f"for (const auto &it : *this->{self.field_name}) {{\n" - if isinstance(self._ti, EnumType): - o += f" buffer.{self._ti.encode_func}({self.number}, static_cast(it), true);\n" + # Special handling for const char* elements (when container_no_template contains "const char") + if "const char" in self._container_no_template: + o = f"for (const char *it : *this->{self.field_name}) {{\n" + o += f" buffer.{self._ti.encode_func}({self.number}, it, strlen(it), true);\n" else: - o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n" + o = f"for (const auto &it : *this->{self.field_name}) {{\n" + if isinstance(self._ti, EnumType): + o += f" buffer.{self._ti.encode_func}({self.number}, static_cast(it), true);\n" + else: + o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n" o += "}" return o o = f"for (auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{\n" @@ -1550,10 +1562,18 @@ class RepeatedTypeInfo(TypeInfo): @property def dump_content(self) -> str: + # Check if this is const char* elements + is_const_char_ptr = ( + self._use_pointer and "const char" in self._container_no_template + ) if self._use_pointer: # For pointer fields, dereference and use the existing helper return _generate_array_dump_content( - self._ti, f"*this->{self.field_name}", self.name, is_bool=False + self._ti, + f"*this->{self.field_name}", + self.name, + is_bool=False, + is_const_char_ptr=is_const_char_ptr, ) return _generate_array_dump_content( self._ti, f"this->{self.field_name}", self.name, is_bool=self._ti_is_bool @@ -1588,9 +1608,14 @@ class RepeatedTypeInfo(TypeInfo): o += f" size.add_precalculated_size({size_expr} * {bytes_per_element});\n" else: # Other types need the actual value - auto_ref = "" if self._ti_is_bool else "&" - o += f" for (const auto {auto_ref}it : {container_ref}) {{\n" - o += f" {self._ti.get_size_calculation('it', True)}\n" + # Special handling for const char* elements + if self._use_pointer and "const char" in self._container_no_template: + o += f" for (const char *it : {container_ref}) {{\n" + o += " size.add_length_force(1, strlen(it));\n" + else: + auto_ref = "" if self._ti_is_bool else "&" + o += f" for (const auto {auto_ref}it : {container_ref}) {{\n" + o += f" {self._ti.get_size_calculation('it', True)}\n" o += " }\n" o += "}" @@ -2542,6 +2567,12 @@ static void dump_field(std::string &out, const char *field_name, StringRef value out.append("\\n"); } +static void dump_field(std::string &out, const char *field_name, const char *value, int indent = 2) { + append_field_prefix(out, field_name, indent); + out.append("'").append(value).append("'"); + out.append("\\n"); +} + template static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) { append_field_prefix(out, field_name, indent); From f6e4c0cb521392247b858ca81a37fd001a5219e3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Oct 2025 22:22:28 -0500 Subject: [PATCH 192/394] [ci] Fix component tests not running when only test files change (#11580) --- script/helpers.py | 16 +++++++++------- tests/script/test_helpers.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/script/helpers.py b/script/helpers.py index 78c11b427e..447d54fa54 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -90,16 +90,18 @@ def get_component_from_path(file_path: str) -> str | None: """Extract component name from a file path. Args: - file_path: Path to a file (e.g., "esphome/components/wifi/wifi.cpp") + file_path: Path to a file (e.g., "esphome/components/wifi/wifi.cpp" + or "tests/components/uart/test.esp32-idf.yaml") Returns: - Component name if path is in components directory, None otherwise + Component name if path is in components or tests directory, None otherwise """ - if not file_path.startswith(ESPHOME_COMPONENTS_PATH): - return None - parts = file_path.split("/") - if len(parts) >= 3: - return parts[2] + if file_path.startswith(ESPHOME_COMPONENTS_PATH) or file_path.startswith( + ESPHOME_TESTS_COMPONENTS_PATH + ): + parts = file_path.split("/") + if len(parts) >= 3 and parts[2]: + return parts[2] return None diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index 63f1f0e600..1046512a14 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -1065,3 +1065,39 @@ def test_parse_list_components_output(output: str, expected: list[str]) -> None: """Test parse_list_components_output function.""" result = helpers.parse_list_components_output(output) assert result == expected + + +@pytest.mark.parametrize( + ("file_path", "expected_component"), + [ + # Component files + ("esphome/components/wifi/wifi.cpp", "wifi"), + ("esphome/components/uart/uart.h", "uart"), + ("esphome/components/api/api_server.cpp", "api"), + ("esphome/components/sensor/sensor.cpp", "sensor"), + # Test files + ("tests/components/uart/test.esp32-idf.yaml", "uart"), + ("tests/components/wifi/test.esp8266-ard.yaml", "wifi"), + ("tests/components/sensor/test.esp32-idf.yaml", "sensor"), + ("tests/components/api/test_api.cpp", "api"), + ("tests/components/uart/common.h", "uart"), + # Non-component files + ("esphome/core/component.cpp", None), + ("esphome/core/helpers.h", None), + ("tests/integration/test_api.py", None), + ("tests/unit_tests/test_helpers.py", None), + ("README.md", None), + ("script/helpers.py", None), + # Edge cases + ("esphome/components/", None), # No component name + ("tests/components/", None), # No component name + ("esphome/components", None), # No trailing slash + ("tests/components", None), # No trailing slash + ], +) +def test_get_component_from_path( + file_path: str, expected_component: str | None +) -> None: + """Test extraction of component names from file paths.""" + result = helpers.get_component_from_path(file_path) + assert result == expected_component From 6cf0a38b8679978eb097f36480511bce4cbd31fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Oct 2025 22:26:27 -0500 Subject: [PATCH 193/394] preen --- esphome/components/fan/fan_traits.h | 4 ++++ esphome/components/hbridge/fan/hbridge_fan.h | 4 +--- esphome/components/speed/fan/speed_fan.h | 4 +--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index 15c951b045..d3873d6af6 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -38,6 +38,10 @@ class FanTraits { const std::vector &supported_preset_modes() const { return this->preset_modes_; } /// Set the preset modes supported by the fan. void set_supported_preset_modes(const std::vector &preset_modes) { this->preset_modes_ = preset_modes; } + /// Set the preset modes supported by the fan (from initializer list). + void set_supported_preset_modes(std::initializer_list preset_modes) { + this->preset_modes_ = preset_modes; + } /// Return if preset modes are supported bool supports_preset_modes() const { return !this->preset_modes_.empty(); } diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h index b5fb7f5daa..cf1bdc9562 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.h +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -1,7 +1,5 @@ #pragma once -#include - #include "esphome/core/automation.h" #include "esphome/components/output/binary_output.h" #include "esphome/components/output/float_output.h" @@ -22,7 +20,7 @@ class HBridgeFan : public Component, public fan::Fan { void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } - void set_preset_modes(const std::vector &presets) { preset_modes_ = presets; } + void set_preset_modes(std::initializer_list presets) { preset_modes_ = presets; } void setup() override; void dump_config() override; diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index 454b7fc136..35fd1c4429 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -1,7 +1,5 @@ #pragma once -#include - #include "esphome/core/component.h" #include "esphome/components/output/binary_output.h" #include "esphome/components/output/float_output.h" @@ -18,7 +16,7 @@ class SpeedFan : public Component, public fan::Fan { void set_output(output::FloatOutput *output) { this->output_ = output; } void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } - void set_preset_modes(const std::vector &presets) { this->preset_modes_ = presets; } + void set_preset_modes(std::initializer_list presets) { this->preset_modes_ = presets; } fan::FanTraits get_traits() override { return this->traits_; } protected: From 4cc41606d1674d92300d91a7e6ab6cf42a95c8bb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Oct 2025 22:40:45 -0500 Subject: [PATCH 194/394] cleanup --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_connection.cpp | 2 +- esphome/components/api/api_pb2.cpp | 8 +++---- esphome/components/api/api_pb2.h | 2 +- esphome/components/fan/fan.cpp | 12 +++++----- esphome/components/fan/fan_traits.h | 24 ++++++------------- .../components/template/fan/template_fan.h | 2 -- 7 files changed, 20 insertions(+), 32 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 0be087b52d..39e4fb0f74 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -425,7 +425,7 @@ message ListEntitiesFanResponse { bool disabled_by_default = 9; string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 11; - repeated string supported_preset_modes = 12 [(container_pointer) = "std::vector"]; + repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "FixedVector"]; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; } // Deprecated in API version 1.6 - only used in deprecated fields diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 382c4acc16..33d5072d9c 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -423,7 +423,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con msg.supports_speed = traits.supports_speed(); msg.supports_direction = traits.supports_direction(); msg.supported_speed_count = traits.supported_speed_count(); - msg.supported_preset_modes = &traits.supported_preset_modes_for_api_(); + msg.supported_preset_modes = &traits.supported_preset_modes(); return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::fan_command(const FanCommandRequest &msg) { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 3472707d3c..0673d35518 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -355,8 +355,8 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(10, this->icon_ref_); #endif buffer.encode_uint32(11, static_cast(this->entity_category)); - for (const auto &it : *this->supported_preset_modes) { - buffer.encode_string(12, it, true); + for (const char *it : *this->supported_preset_modes) { + buffer.encode_string(12, it, strlen(it), true); } #ifdef USE_DEVICES buffer.encode_uint32(13, this->device_id); @@ -376,8 +376,8 @@ void ListEntitiesFanResponse::calculate_size(ProtoSize &size) const { #endif size.add_uint32(1, static_cast(this->entity_category)); if (!this->supported_preset_modes->empty()) { - for (const auto &it : *this->supported_preset_modes) { - size.add_length_force(1, it.size()); + for (const char *it : *this->supported_preset_modes) { + size.add_length_force(1, strlen(it)); } } #ifdef USE_DEVICES diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 43018ef32c..47224a0fc4 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -725,7 +725,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage { bool supports_speed{false}; bool supports_direction{false}; int32_t supported_speed_count{0}; - const std::vector *supported_preset_modes{}; + const FixedVector *supported_preset_modes{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index cf1ec3d6ae..10705518ea 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -53,7 +53,7 @@ void FanCall::validate_() { const auto &preset_modes = traits.supported_preset_modes(); bool found = false; for (const auto &mode : preset_modes) { - if (mode == this->preset_mode_) { + if (strcmp(mode, this->preset_mode_.c_str()) == 0) { found = true; break; } @@ -103,7 +103,7 @@ FanCall FanRestoreState::to_call(Fan &fan) { // Use stored preset index to get preset name const auto &preset_modes = fan.get_traits().supported_preset_modes(); if (this->preset_mode < preset_modes.size()) { - call.set_preset_mode(*std::next(preset_modes.begin(), this->preset_mode)); + call.set_preset_mode(preset_modes[this->preset_mode]); } } return call; @@ -118,7 +118,7 @@ void FanRestoreState::apply(Fan &fan) { // Use stored preset index to get preset name const auto &preset_modes = fan.get_traits().supported_preset_modes(); if (this->preset_mode < preset_modes.size()) { - fan.preset_mode = *std::next(preset_modes.begin(), this->preset_mode); + fan.preset_mode = preset_modes[this->preset_mode]; } } fan.publish_state(); @@ -200,7 +200,7 @@ void Fan::save_state_() { // Store index of current preset mode size_t i = 0; for (const auto &mode : preset_modes) { - if (mode == this->preset_mode) { + if (strcmp(mode, this->preset_mode.c_str()) == 0) { state.preset_mode = i; break; } @@ -228,8 +228,8 @@ void Fan::dump_traits_(const char *tag, const char *prefix) { } if (traits.supports_preset_modes()) { ESP_LOGCONFIG(tag, "%s Supported presets:", prefix); - for (const std::string &s : traits.supported_preset_modes()) - ESP_LOGCONFIG(tag, "%s - %s", prefix, s.c_str()); + for (const char *s : traits.supported_preset_modes()) + ESP_LOGCONFIG(tag, "%s - %s", prefix, s); } } diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index d3873d6af6..e62b54cbdb 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -1,6 +1,7 @@ #pragma once -#include +#include "esphome/core/helpers.h" +#include namespace esphome { @@ -35,31 +36,20 @@ class FanTraits { /// Set whether this fan supports changing direction void set_direction(bool direction) { this->direction_ = direction; } /// Return the preset modes supported by the fan. - const std::vector &supported_preset_modes() const { return this->preset_modes_; } - /// Set the preset modes supported by the fan. - void set_supported_preset_modes(const std::vector &preset_modes) { this->preset_modes_ = preset_modes; } + const FixedVector &supported_preset_modes() const { return this->preset_modes_; } /// Set the preset modes supported by the fan (from initializer list). - void set_supported_preset_modes(std::initializer_list preset_modes) { - this->preset_modes_ = preset_modes; - } + void set_supported_preset_modes(const std::initializer_list &preset_modes); + /// Set the preset modes supported by the fan (from FixedVector). + void set_supported_preset_modes(const FixedVector &preset_modes); /// Return if preset modes are supported bool supports_preset_modes() const { return !this->preset_modes_.empty(); } protected: -#ifdef USE_API - // The API connection is a friend class to access internal methods - friend class api::APIConnection; - // This method returns a reference to the internal preset modes. - // It is used by the API to avoid copying data when encoding messages. - // Warning: Do not use this method outside of the API connection code. - // It returns a reference to internal data that can be invalidated. - const std::vector &supported_preset_modes_for_api_() const { return this->preset_modes_; } -#endif bool oscillation_{false}; bool speed_{false}; bool direction_{false}; int speed_count_{}; - std::vector preset_modes_{}; + FixedVector preset_modes_{}; }; } // namespace fan diff --git a/esphome/components/template/fan/template_fan.h b/esphome/components/template/fan/template_fan.h index 5d780f61f0..04d72b6939 100644 --- a/esphome/components/template/fan/template_fan.h +++ b/esphome/components/template/fan/template_fan.h @@ -1,7 +1,5 @@ #pragma once -#include - #include "esphome/core/component.h" #include "esphome/components/fan/fan.h" From cc815fd6831240778b0c3a4b063ac952e6c61013 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Oct 2025 22:40:56 -0500 Subject: [PATCH 195/394] cleanup --- esphome/components/template/fan/template_fan.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/template/fan/template_fan.h b/esphome/components/template/fan/template_fan.h index 04d72b6939..7cc4d4b657 100644 --- a/esphome/components/template/fan/template_fan.h +++ b/esphome/components/template/fan/template_fan.h @@ -14,7 +14,7 @@ class TemplateFan : public Component, public fan::Fan { void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; } void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; } void set_speed_count(int count) { this->speed_count_ = count; } - void set_preset_modes(const std::initializer_list &presets) { this->preset_modes_ = presets; } + void set_preset_modes(std::initializer_list presets) { this->preset_modes_ = presets; } fan::FanTraits get_traits() override { return this->traits_; } protected: @@ -24,7 +24,7 @@ class TemplateFan : public Component, public fan::Fan { bool has_direction_{false}; int speed_count_{0}; fan::FanTraits traits_; - std::vector preset_modes_{}; + FixedVector preset_modes_{}; }; } // namespace template_ From 47cbe74453ba42283b3bbd90b94d1f0ac747d73c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Oct 2025 22:41:13 -0500 Subject: [PATCH 196/394] cleanup --- esphome/components/hbridge/fan/hbridge_fan.h | 4 ++-- esphome/components/speed/fan/speed_fan.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h index cf1bdc9562..4674a81928 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.h +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -20,7 +20,7 @@ class HBridgeFan : public Component, public fan::Fan { void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } - void set_preset_modes(std::initializer_list presets) { preset_modes_ = presets; } + void set_preset_modes(std::initializer_list presets) { preset_modes_ = presets; } void setup() override; void dump_config() override; @@ -36,7 +36,7 @@ class HBridgeFan : public Component, public fan::Fan { int speed_count_{}; DecayMode decay_mode_{DECAY_MODE_SLOW}; fan::FanTraits traits_; - std::vector preset_modes_{}; + FixedVector preset_modes_{}; void control(const fan::FanCall &call) override; void write_state_(); diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index 35fd1c4429..e9aea3c62e 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -16,7 +16,7 @@ class SpeedFan : public Component, public fan::Fan { void set_output(output::FloatOutput *output) { this->output_ = output; } void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } - void set_preset_modes(std::initializer_list presets) { this->preset_modes_ = presets; } + void set_preset_modes(std::initializer_list presets) { this->preset_modes_ = presets; } fan::FanTraits get_traits() override { return this->traits_; } protected: @@ -28,7 +28,7 @@ class SpeedFan : public Component, public fan::Fan { output::BinaryOutput *direction_{nullptr}; int speed_count_{}; fan::FanTraits traits_; - std::vector preset_modes_{}; + FixedVector preset_modes_{}; }; } // namespace speed From bb99f68d33b127979dabf8f7be21ede16eb1661c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Oct 2025 22:47:36 -0500 Subject: [PATCH 197/394] cleanup --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_pb2.h | 2 +- esphome/components/fan/fan_traits.h | 20 ++++++++----------- esphome/components/hbridge/fan/hbridge_fan.h | 2 +- esphome/components/speed/fan/speed_fan.h | 2 +- .../components/template/fan/template_fan.h | 2 +- 6 files changed, 13 insertions(+), 17 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 39e4fb0f74..20645fc47b 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -425,7 +425,7 @@ message ListEntitiesFanResponse { bool disabled_by_default = 9; string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 11; - repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "FixedVector"]; + repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "std::vector"]; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; } // Deprecated in API version 1.6 - only used in deprecated fields diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 47224a0fc4..89f16044d7 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -725,7 +725,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage { bool supports_speed{false}; bool supports_direction{false}; int32_t supported_speed_count{0}; - const FixedVector *supported_preset_modes{}; + const std::vector *supported_preset_modes{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index e62b54cbdb..bfb17a05ab 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -1,16 +1,10 @@ #pragma once -#include "esphome/core/helpers.h" +#include #include namespace esphome { -#ifdef USE_API -namespace api { -class APIConnection; -} // namespace api -#endif - namespace fan { class FanTraits { @@ -36,11 +30,13 @@ class FanTraits { /// Set whether this fan supports changing direction void set_direction(bool direction) { this->direction_ = direction; } /// Return the preset modes supported by the fan. - const FixedVector &supported_preset_modes() const { return this->preset_modes_; } + const std::vector &supported_preset_modes() const { return this->preset_modes_; } /// Set the preset modes supported by the fan (from initializer list). - void set_supported_preset_modes(const std::initializer_list &preset_modes); - /// Set the preset modes supported by the fan (from FixedVector). - void set_supported_preset_modes(const FixedVector &preset_modes); + void set_supported_preset_modes(std::initializer_list preset_modes) { + this->preset_modes_ = preset_modes; + } + /// Set the preset modes supported by the fan (from vector). + void set_supported_preset_modes(const std::vector &preset_modes) { this->preset_modes_ = preset_modes; } /// Return if preset modes are supported bool supports_preset_modes() const { return !this->preset_modes_.empty(); } @@ -49,7 +45,7 @@ class FanTraits { bool speed_{false}; bool direction_{false}; int speed_count_{}; - FixedVector preset_modes_{}; + std::vector preset_modes_{}; }; } // namespace fan diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h index 4674a81928..143c7c1853 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.h +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -36,7 +36,7 @@ class HBridgeFan : public Component, public fan::Fan { int speed_count_{}; DecayMode decay_mode_{DECAY_MODE_SLOW}; fan::FanTraits traits_; - FixedVector preset_modes_{}; + std::vector preset_modes_{}; void control(const fan::FanCall &call) override; void write_state_(); diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index e9aea3c62e..e9a389e0f3 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -28,7 +28,7 @@ class SpeedFan : public Component, public fan::Fan { output::BinaryOutput *direction_{nullptr}; int speed_count_{}; fan::FanTraits traits_; - FixedVector preset_modes_{}; + std::vector preset_modes_{}; }; } // namespace speed diff --git a/esphome/components/template/fan/template_fan.h b/esphome/components/template/fan/template_fan.h index 7cc4d4b657..b09352f4d4 100644 --- a/esphome/components/template/fan/template_fan.h +++ b/esphome/components/template/fan/template_fan.h @@ -24,7 +24,7 @@ class TemplateFan : public Component, public fan::Fan { bool has_direction_{false}; int speed_count_{0}; fan::FanTraits traits_; - FixedVector preset_modes_{}; + std::vector preset_modes_{}; }; } // namespace template_ From e4aec7f413d54fe45545adc2139253bb6921ae12 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Oct 2025 22:57:50 -0500 Subject: [PATCH 198/394] make sure no dangling --- esphome/components/fan/fan.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 10705518ea..b5eb1121e3 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -101,7 +101,8 @@ FanCall FanRestoreState::to_call(Fan &fan) { if (fan.get_traits().supports_preset_modes()) { // Use stored preset index to get preset name - const auto &preset_modes = fan.get_traits().supported_preset_modes(); + auto traits = fan.get_traits(); + const auto &preset_modes = traits.supported_preset_modes(); if (this->preset_mode < preset_modes.size()) { call.set_preset_mode(preset_modes[this->preset_mode]); } @@ -116,7 +117,8 @@ void FanRestoreState::apply(Fan &fan) { if (fan.get_traits().supports_preset_modes()) { // Use stored preset index to get preset name - const auto &preset_modes = fan.get_traits().supported_preset_modes(); + auto traits = fan.get_traits(); + const auto &preset_modes = traits.supported_preset_modes(); if (this->preset_mode < preset_modes.size()) { fan.preset_mode = preset_modes[this->preset_mode]; } @@ -196,7 +198,8 @@ void Fan::save_state_() { state.direction = this->direction; if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) { - const auto &preset_modes = this->get_traits().supported_preset_modes(); + auto traits = this->get_traits(); + const auto &preset_modes = traits.supported_preset_modes(); // Store index of current preset mode size_t i = 0; for (const auto &mode : preset_modes) { From b635689c291acd1af430d8369f843131a81518e6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Oct 2025 23:01:28 -0500 Subject: [PATCH 199/394] make sure no dangling --- esphome/components/fan/fan.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index b5eb1121e3..3a02161cd0 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -191,14 +191,15 @@ void Fan::save_state_() { return; } + auto traits = this->get_traits(); + FanRestoreState state{}; state.state = this->state; state.oscillating = this->oscillating; state.speed = this->speed; state.direction = this->direction; - if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) { - auto traits = this->get_traits(); + if (traits.supports_preset_modes() && !this->preset_mode.empty()) { const auto &preset_modes = traits.supported_preset_modes(); // Store index of current preset mode size_t i = 0; From 372c162e6baad33479e92a0273d78ce95a344889 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Oct 2025 23:02:14 -0500 Subject: [PATCH 200/394] make sure no dangling --- esphome/components/fan/fan.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 3a02161cd0..5b4f437f99 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -99,9 +99,9 @@ FanCall FanRestoreState::to_call(Fan &fan) { call.set_speed(this->speed); call.set_direction(this->direction); - if (fan.get_traits().supports_preset_modes()) { + auto traits = fan.get_traits(); + if (traits.supports_preset_modes()) { // Use stored preset index to get preset name - auto traits = fan.get_traits(); const auto &preset_modes = traits.supported_preset_modes(); if (this->preset_mode < preset_modes.size()) { call.set_preset_mode(preset_modes[this->preset_mode]); @@ -115,9 +115,9 @@ void FanRestoreState::apply(Fan &fan) { fan.speed = this->speed; fan.direction = this->direction; - if (fan.get_traits().supports_preset_modes()) { + auto traits = fan.get_traits(); + if (traits.supports_preset_modes()) { // Use stored preset index to get preset name - auto traits = fan.get_traits(); const auto &preset_modes = traits.supported_preset_modes(); if (this->preset_mode < preset_modes.size()) { fan.preset_mode = preset_modes[this->preset_mode]; From 71695567221ffe1bdd3575a2e8878e456ec70f23 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 04:46:47 +0000 Subject: [PATCH 201/394] Bump aioesphomeapi from 42.4.0 to 42.5.0 (#11597) 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 b0d7d62c36..660b18c933 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.1.0 click==8.1.7 esphome-dashboard==20251013.0 -aioesphomeapi==42.4.0 +aioesphomeapi==42.5.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.18.16 # dashboard_import From 90956f7417bfb02f4fc665f622f9e491c523faa9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Oct 2025 23:56:44 -0500 Subject: [PATCH 202/394] [e131] Replace std::set with std::vector to reduce flash usage --- esphome/components/e131/e131.cpp | 13 +++++++++---- esphome/components/e131/e131.h | 4 +--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index d18d945cec..c10c88faf2 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -3,6 +3,8 @@ #include "e131_addressable_light_effect.h" #include "esphome/core/log.h" +#include + namespace esphome { namespace e131 { @@ -76,14 +78,14 @@ void E131Component::loop() { } void E131Component::add_effect(E131AddressableLightEffect *light_effect) { - if (light_effects_.count(light_effect)) { + if (std::find(light_effects_.begin(), light_effects_.end(), light_effect) != light_effects_.end()) { return; } ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(), light_effect->get_last_universe()); - light_effects_.insert(light_effect); + light_effects_.push_back(light_effect); for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { join_(universe); @@ -91,14 +93,17 @@ void E131Component::add_effect(E131AddressableLightEffect *light_effect) { } void E131Component::remove_effect(E131AddressableLightEffect *light_effect) { - if (!light_effects_.count(light_effect)) { + auto it = std::find(light_effects_.begin(), light_effects_.end(), light_effect); + if (it == light_effects_.end()) { return; } ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(), light_effect->get_last_universe()); - light_effects_.erase(light_effect); + // Swap with last element and pop for O(1) removal (order doesn't matter) + *it = light_effects_.back(); + light_effects_.pop_back(); for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { leave_(universe); diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index d0e38fa98c..831138a545 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -7,7 +7,6 @@ #include #include #include -#include #include namespace esphome { @@ -47,9 +46,8 @@ class E131Component : public esphome::Component { E131ListenMethod listen_method_{E131_MULTICAST}; std::unique_ptr socket_; - std::set light_effects_; + std::vector light_effects_; std::map universe_consumers_; - std::map universe_packets_; }; } // namespace e131 From b6c9ece0e67230f34dcb60b03ea909baef0d7218 Mon Sep 17 00:00:00 2001 From: Kent Gibson Date: Wed, 29 Oct 2025 13:10:36 +0800 Subject: [PATCH 203/394] template_alarm_control_panel readability improvements (#11593) --- .../template_alarm_control_panel.cpp | 75 +++++++++---------- 1 file changed, 36 insertions(+), 39 deletions(-) diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index d1562ee82f..f7e9872ce1 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -25,6 +25,20 @@ void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, this->sensor_data_.push_back(sd); this->sensor_map_[sensor].store_index = this->next_store_index_++; }; + +static const LogString *sensor_type_to_string(AlarmSensorType type) { + switch (type) { + case ALARM_SENSOR_TYPE_INSTANT: + return LOG_STR("instant"); + case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER: + return LOG_STR("delayed_follower"); + case ALARM_SENSOR_TYPE_INSTANT_ALWAYS: + return LOG_STR("instant_always"); + case ALARM_SENSOR_TYPE_DELAYED: + default: + return LOG_STR("delayed"); + } +} #endif void TemplateAlarmControlPanel::dump_config() { @@ -46,35 +60,20 @@ void TemplateAlarmControlPanel::dump_config() { " Supported Features: %" PRIu32, (this->pending_time_ / 1000), (this->trigger_time_ / 1000), this->get_supported_features()); #ifdef USE_BINARY_SENSOR - for (auto sensor_info : this->sensor_map_) { - ESP_LOGCONFIG(TAG, " Binary Sensor:"); + for (auto const &[sensor, info] : this->sensor_map_) { ESP_LOGCONFIG(TAG, + " Binary Sensor:\n" " Name: %s\n" + " Type: %s\n" " Armed home bypass: %s\n" " Armed night bypass: %s\n" " Auto bypass: %s\n" " Chime mode: %s", - sensor_info.first->get_name().c_str(), - TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME), - TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT), - TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_AUTO), - TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_CHIME)); - const char *sensor_type; - switch (sensor_info.second.type) { - case ALARM_SENSOR_TYPE_INSTANT: - sensor_type = "instant"; - break; - case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER: - sensor_type = "delayed_follower"; - break; - case ALARM_SENSOR_TYPE_INSTANT_ALWAYS: - sensor_type = "instant_always"; - break; - case ALARM_SENSOR_TYPE_DELAYED: - default: - sensor_type = "delayed"; - } - ESP_LOGCONFIG(TAG, " Sensor type: %s", sensor_type); + sensor->get_name().c_str(), LOG_STR_ARG(sensor_type_to_string(info.type)), + TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME), + TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT), + TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO), + TRUEFALSE(info.flags & BINARY_SENSOR_MODE_CHIME)); } #endif } @@ -123,39 +122,37 @@ void TemplateAlarmControlPanel::loop() { bool instant_sensor_faulted = false; #ifdef USE_BINARY_SENSOR - // Test all of the sensors in the list regardless of the alarm panel state - for (auto sensor_info : this->sensor_map_) { + // Test all of the sensors regardless of the alarm panel state + for (auto const &[sensor, info] : this->sensor_map_) { // Check for chime zones - if ((sensor_info.second.flags & BINARY_SENSOR_MODE_CHIME)) { + if (info.flags & BINARY_SENSOR_MODE_CHIME) { // Look for the transition from closed to open - if ((!this->sensor_data_[sensor_info.second.store_index].last_chime_state) && (sensor_info.first->state)) { + if ((!this->sensor_data_[info.store_index].last_chime_state) && (sensor->state)) { // Must be disarmed to chime if (this->current_state_ == ACP_STATE_DISARMED) { this->chime_callback_.call(); } } // Record the sensor state change - this->sensor_data_[sensor_info.second.store_index].last_chime_state = sensor_info.first->state; + this->sensor_data_[info.store_index].last_chime_state = sensor->state; } // Check for faulted sensors - if (sensor_info.first->state) { // Sensor triggered? + if (sensor->state) { // Skip if auto bypassed if (std::count(this->bypassed_sensor_indicies_.begin(), this->bypassed_sensor_indicies_.end(), - sensor_info.second.store_index) == 1) { + info.store_index) == 1) { continue; } // Skip if bypass armed home - if (this->current_state_ == ACP_STATE_ARMED_HOME && - (sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)) { + if ((this->current_state_ == ACP_STATE_ARMED_HOME) && (info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)) { continue; } // Skip if bypass armed night - if (this->current_state_ == ACP_STATE_ARMED_NIGHT && - (sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT)) { + if ((this->current_state_ == ACP_STATE_ARMED_NIGHT) && (info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT)) { continue; } - switch (sensor_info.second.type) { + switch (info.type) { case ALARM_SENSOR_TYPE_INSTANT_ALWAYS: next_state = ACP_STATE_TRIGGERED; [[fallthrough]]; @@ -247,11 +244,11 @@ void TemplateAlarmControlPanel::arm_(optional code, alarm_control_p void TemplateAlarmControlPanel::bypass_before_arming() { #ifdef USE_BINARY_SENSOR - for (auto sensor_info : this->sensor_map_) { + for (auto const &[sensor, info] : this->sensor_map_) { // Check for faulted bypass_auto sensors and remove them from monitoring - if ((sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor_info.first->state)) { - ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", sensor_info.first->get_name().c_str()); - this->bypassed_sensor_indicies_.push_back(sensor_info.second.store_index); + if ((info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor->state)) { + ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", sensor->get_name().c_str()); + this->bypassed_sensor_indicies_.push_back(info.store_index); } } #endif From 09d89000ad3787e1419c662d1b66d70918443074 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 29 Oct 2025 18:14:02 +1300 Subject: [PATCH 204/394] [core] Remove deprecated schema constants (#11591) --- .../alarm_control_panel/__init__.py | 6 ----- esphome/components/binary_sensor/__init__.py | 5 ---- esphome/components/button/__init__.py | 5 ---- esphome/components/climate/__init__.py | 5 ---- esphome/components/climate_ir/__init__.py | 23 +------------------ esphome/components/cover/__init__.py | 5 ---- esphome/components/event/__init__.py | 5 ---- esphome/components/fan/__init__.py | 4 ---- esphome/components/lock/__init__.py | 5 ---- esphome/components/media_player/__init__.py | 4 ---- esphome/components/number/__init__.py | 5 ---- esphome/components/select/__init__.py | 5 ---- esphome/components/sensor/__init__.py | 5 ---- esphome/components/switch/__init__.py | 5 ---- esphome/components/text/__init__.py | 5 ---- esphome/components/text_sensor/__init__.py | 5 ---- esphome/components/update/__init__.py | 5 ---- esphome/components/valve/__init__.py | 5 ---- esphome/config_validation.py | 23 ------------------- script/build_language_schema.py | 2 +- 20 files changed, 2 insertions(+), 130 deletions(-) diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py index 174a9d9e0a..b1e2252ce7 100644 --- a/esphome/components/alarm_control_panel/__init__.py +++ b/esphome/components/alarm_control_panel/__init__.py @@ -172,12 +172,6 @@ def alarm_control_panel_schema( return _ALARM_CONTROL_PANEL_SCHEMA.extend(schema) -# Remove before 2025.11.0 -ALARM_CONTROL_PANEL_SCHEMA = alarm_control_panel_schema(AlarmControlPanel) -ALARM_CONTROL_PANEL_SCHEMA.add_extra( - cv.deprecated_schema_constant("alarm_control_panel") -) - ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id( { cv.GenerateID(): cv.use_id(AlarmControlPanel), diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 8892b57e6e..cbf935a501 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -548,11 +548,6 @@ def binary_sensor_schema( return _BINARY_SENSOR_SCHEMA.extend(schema) -# Remove before 2025.11.0 -BINARY_SENSOR_SCHEMA = binary_sensor_schema() -BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor")) - - async def setup_binary_sensor_core_(var, config): await setup_entity(var, config, "binary_sensor") diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index e1ac875cb0..d2f143b97e 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -84,11 +84,6 @@ def button_schema( return _BUTTON_SCHEMA.extend(schema) -# Remove before 2025.11.0 -BUTTON_SCHEMA = button_schema(Button) -BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button")) - - async def setup_button_core_(var, config): await setup_entity(var, config, "button") diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index c0c33d7242..5824e68141 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -270,11 +270,6 @@ def climate_schema( return _CLIMATE_SCHEMA.extend(schema) -# Remove before 2025.11.0 -CLIMATE_SCHEMA = climate_schema(Climate) -CLIMATE_SCHEMA.add_extra(cv.deprecated_schema_constant("climate")) - - async def setup_climate_core_(var, config): await setup_entity(var, config, "climate") diff --git a/esphome/components/climate_ir/__init__.py b/esphome/components/climate_ir/__init__.py index 312b2ad900..6d66abf4cd 100644 --- a/esphome/components/climate_ir/__init__.py +++ b/esphome/components/climate_ir/__init__.py @@ -1,10 +1,9 @@ import logging -from esphome import core import esphome.codegen as cg from esphome.components import climate, remote_base, sensor import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT +from esphome.const import CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT from esphome.cpp_generator import MockObjClass _LOGGER = logging.getLogger(__name__) @@ -52,26 +51,6 @@ def climate_ir_with_receiver_schema( ) -# Remove before 2025.11.0 -def deprecated_schema_constant(config): - type: str = "unknown" - if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID): - type = str(id.type).split("::", maxsplit=1)[0] - _LOGGER.warning( - "Using `climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. " - "Please use `climate_ir.climate_ir_with_receiver_schema(...)` instead. " - "If you are seeing this, report an issue to the external_component author and ask them to update it. " - "https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. " - "Component using this schema: %s", - type, - ) - return config - - -CLIMATE_IR_WITH_RECEIVER_SCHEMA = climate_ir_with_receiver_schema(ClimateIR) -CLIMATE_IR_WITH_RECEIVER_SCHEMA.add_extra(deprecated_schema_constant) - - async def register_climate_ir(var, config): await cg.register_component(var, config) await remote_base.register_transmittable(var, config) diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index bec6dcbdac..383daee083 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -151,11 +151,6 @@ def cover_schema( return _COVER_SCHEMA.extend(schema) -# Remove before 2025.11.0 -COVER_SCHEMA = cover_schema(Cover) -COVER_SCHEMA.add_extra(cv.deprecated_schema_constant("cover")) - - async def setup_cover_core_(var, config): await setup_entity(var, config, "cover") diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index 449cc48625..e2b69ba872 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -85,11 +85,6 @@ def event_schema( return _EVENT_SCHEMA.extend(schema) -# Remove before 2025.11.0 -EVENT_SCHEMA = event_schema() -EVENT_SCHEMA.add_extra(cv.deprecated_schema_constant("event")) - - async def setup_event_core_(var, config, *, event_types: list[str]): await setup_entity(var, config, "event") diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 245c9f04b4..35a351e8f1 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -189,10 +189,6 @@ def fan_schema( return _FAN_SCHEMA.extend(schema) -# Remove before 2025.11.0 -FAN_SCHEMA = fan_schema(Fan) -FAN_SCHEMA.add_extra(cv.deprecated_schema_constant("fan")) - _PRESET_MODES_SCHEMA = cv.All( cv.ensure_list(cv.string_strict), cv.Length(min=1), diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index 04c1586ddd..9d893d3ad9 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -91,11 +91,6 @@ def lock_schema( return _LOCK_SCHEMA.extend(schema) -# Remove before 2025.11.0 -LOCK_SCHEMA = lock_schema() -LOCK_SCHEMA.add_extra(cv.deprecated_schema_constant("lock")) - - async def _setup_lock_core(var, config): await setup_entity(var, config, "lock") diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index 70c7cf7a56..c6ffe50d79 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -192,10 +192,6 @@ def media_player_schema( return _MEDIA_PLAYER_SCHEMA.extend(schema) -# Remove before 2025.11.0 -MEDIA_PLAYER_SCHEMA = media_player_schema(MediaPlayer) -MEDIA_PLAYER_SCHEMA.add_extra(cv.deprecated_schema_constant("media_player")) - MEDIA_PLAYER_ACTION_SCHEMA = automation.maybe_simple_id( cv.Schema( { diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index ac0329fcc6..368b431d7b 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -238,11 +238,6 @@ def number_schema( return _NUMBER_SCHEMA.extend(schema) -# Remove before 2025.11.0 -NUMBER_SCHEMA = number_schema(Number) -NUMBER_SCHEMA.add_extra(cv.deprecated_schema_constant("number")) - - async def setup_number_core_( var, config, *, min_value: float, max_value: float, step: float ): diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index c7146df9fb..7c50fe02c0 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -86,11 +86,6 @@ def select_schema( return _SELECT_SCHEMA.extend(schema) -# Remove before 2025.11.0 -SELECT_SCHEMA = select_schema(Select) -SELECT_SCHEMA.add_extra(cv.deprecated_schema_constant("select")) - - async def setup_select_core_(var, config, *, options: list[str]): await setup_entity(var, config, "select") diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 41ac3516b9..e8fec222a1 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -369,11 +369,6 @@ def sensor_schema( return _SENSOR_SCHEMA.extend(schema) -# Remove before 2025.11.0 -SENSOR_SCHEMA = sensor_schema() -SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("sensor")) - - @FILTER_REGISTRY.register("offset", OffsetFilter, cv.templatable(cv.float_)) async def offset_filter_to_code(config, filter_id): template_ = await cg.templatable(config, [], float) diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 0e7b35b373..e9473012cf 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -139,11 +139,6 @@ def switch_schema( return _SWITCH_SCHEMA.extend(schema) -# Remove before 2025.11.0 -SWITCH_SCHEMA = switch_schema(Switch) -SWITCH_SCHEMA.add_extra(cv.deprecated_schema_constant("switch")) - - async def setup_switch_core_(var, config): await setup_entity(var, config, "switch") diff --git a/esphome/components/text/__init__.py b/esphome/components/text/__init__.py index 1baacc239f..9ceea0dfdf 100644 --- a/esphome/components/text/__init__.py +++ b/esphome/components/text/__init__.py @@ -84,11 +84,6 @@ def text_schema( return _TEXT_SCHEMA.extend(schema) -# Remove before 2025.11.0 -TEXT_SCHEMA = text_schema() -TEXT_SCHEMA.add_extra(cv.deprecated_schema_constant("text")) - - async def setup_text_core_( var, config, diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index adc8a76fcd..0d22400a8e 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -193,11 +193,6 @@ def text_sensor_schema( return _TEXT_SENSOR_SCHEMA.extend(schema) -# Remove before 2025.11.0 -TEXT_SENSOR_SCHEMA = text_sensor_schema() -TEXT_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("text_sensor")) - - async def build_filters(config): return await cg.build_registry_list(FILTER_REGISTRY, config) diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py index 35fc4eaf1d..7a381c85a8 100644 --- a/esphome/components/update/__init__.py +++ b/esphome/components/update/__init__.py @@ -84,11 +84,6 @@ def update_schema( return _UPDATE_SCHEMA.extend(schema) -# Remove before 2025.11.0 -UPDATE_SCHEMA = update_schema() -UPDATE_SCHEMA.add_extra(cv.deprecated_schema_constant("update")) - - async def setup_update_core_(var, config): await setup_entity(var, config, "update") diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index 6f31fc3a20..73e907eb0f 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -129,11 +129,6 @@ def valve_schema( return _VALVE_SCHEMA.extend(schema) -# Remove before 2025.11.0 -VALVE_SCHEMA = valve_schema() -VALVE_SCHEMA.add_extra(cv.deprecated_schema_constant("valve")) - - async def _setup_valve_core(var, config): await setup_entity(var, config, "valve") diff --git a/esphome/config_validation.py b/esphome/config_validation.py index c613a984c4..359b257992 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -2195,26 +2195,3 @@ def rename_key(old_key, new_key): return config return validator - - -# Remove before 2025.11.0 -def deprecated_schema_constant(entity_type: str): - def validator(config): - type: str = "unknown" - if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID): - type = str(id.type).split("::", maxsplit=1)[0] - _LOGGER.warning( - "Using `%s.%s_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. " - "Please use `%s.%s_schema(...)` instead. " - "If you are seeing this, report an issue to the external_component author and ask them to update it. " - "https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. " - "Component using this schema: %s", - entity_type, - entity_type.upper(), - entity_type, - entity_type, - type, - ) - return config - - return validator diff --git a/script/build_language_schema.py b/script/build_language_schema.py index 1ffe3c2873..c9501cb193 100755 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -300,7 +300,7 @@ def fix_remote_receiver(): remote_receiver_schema["CONFIG_SCHEMA"] = { "type": "schema", "schema": { - "extends": ["binary_sensor.BINARY_SENSOR_SCHEMA", "core.COMPONENT_SCHEMA"], + "extends": ["binary_sensor._BINARY_SENSOR_SCHEMA", "core.COMPONENT_SCHEMA"], "config_vars": output["remote_base"].pop("binary"), }, } From 59a216bfcbacaffb9cdd10194b657d76b4c8bde0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 00:19:47 -0500 Subject: [PATCH 205/394] Bump github/codeql-action from 4.30.9 to 4.31.0 (#11522) 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 c24900d378..6b940eed8a 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@16140ae1a102900babc80a33c44059580f687047 # v4.30.9 + uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 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@16140ae1a102900babc80a33c44059580f687047 # v4.30.9 + uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 with: category: "/language:${{matrix.language}}" From 33e7a2101bbbcc39111b34740a1563f23fb97bc5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 00:20:05 -0500 Subject: [PATCH 206/394] Bump actions/upload-artifact from 4.6.2 to 5.0.0 (#11520) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-api-proto.yml | 2 +- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index c122859442..400373679f 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -62,7 +62,7 @@ jobs: run: git diff - if: failure() name: Archive artifacts - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: generated-proto-files path: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 655e28e3b3..63a7aaa0bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -849,7 +849,7 @@ jobs: fi - name: Upload memory analysis JSON - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: memory-analysis-target path: memory-analysis-target.json @@ -913,7 +913,7 @@ jobs: --platform "$platform" - name: Upload memory analysis JSON - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: memory-analysis-pr path: memory-analysis-pr.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2b3b3bdc1b..92949d72e1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -138,7 +138,7 @@ jobs: # version: ${{ needs.init.outputs.tag }} - name: Upload digests - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: digests-${{ matrix.platform.arch }} path: /tmp/digests From 7549ca4d39d0c735b72f898cbbd670ca3b681307 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 00:20:13 -0500 Subject: [PATCH 207/394] Bump actions/download-artifact from 5.0.0 to 6.0.0 (#11521) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63a7aaa0bf..bd45adb78b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -943,13 +943,13 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Download target analysis JSON - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: name: memory-analysis-target path: ./memory-analysis continue-on-error: true - name: Download PR analysis JSON - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: name: memory-analysis-pr path: ./memory-analysis diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 92949d72e1..75d88abf29 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -171,7 +171,7 @@ jobs: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Download digests - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: pattern: digests-* path: /tmp/digests From f29021b5efd43e34c85fe4cd905ebea7d669923b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 05:23:42 +0000 Subject: [PATCH 208/394] Bump ruff from 0.14.1 to 0.14.2 (#11519) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9e0e71d388..f7e4a688e0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.1 + rev: v0.14.2 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 5f94329e3f..a11992b0fd 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.2 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.14.1 # also change in .pre-commit-config.yaml when updating +ruff==0.14.2 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.0 # also change in .pre-commit-config.yaml when updating pre-commit From 66cf7c3a3b30a0441cb2c660bcb19bbb2c2a9943 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 01:07:48 -0500 Subject: [PATCH 209/394] [lvgl] Fix nested lambdas in automations unable to access parameters (#11583) Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com> --- esphome/components/lvgl/defines.py | 22 +++++++++++++-- esphome/components/lvgl/lv_validation.py | 21 ++++++++++++--- esphome/components/lvgl/lvcode.py | 13 ++++++--- esphome/components/lvgl/sensor/__init__.py | 3 +-- tests/components/lvgl/common.yaml | 31 ++++++++++++++++++++++ tests/components/lvgl/lvgl-package.yaml | 23 ++++++++++++++++ 6 files changed, 103 insertions(+), 10 deletions(-) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 6464824c64..7fbb6de071 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -5,6 +5,7 @@ Constants already defined in esphome.const are not duplicated here and must be i """ import logging +from typing import TYPE_CHECKING, Any from esphome import codegen as cg, config_validation as cv from esphome.const import CONF_ITEMS @@ -12,6 +13,7 @@ from esphome.core import ID, Lambda from esphome.cpp_generator import LambdaExpression, MockObj from esphome.cpp_types import uint32 from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor +from esphome.types import Expression, SafeExpType from .helpers import requires_component @@ -42,7 +44,13 @@ def static_cast(type, value): def call_lambda(lamb: LambdaExpression): expr = lamb.content.strip() if expr.startswith("return") and expr.endswith(";"): - return expr[6:][:-1].strip() + return expr[6:-1].strip() + # If lambda has parameters, call it with those parameter names + # Parameter names come from hardcoded component code (like "x", "it", "event") + # not from user input, so they're safe to use directly + if lamb.parameters and lamb.parameters.parameters: + param_names = ", ".join(str(param.id) for param in lamb.parameters.parameters) + return f"{lamb}({param_names})" return f"{lamb}()" @@ -65,10 +73,20 @@ class LValidator: return cv.returning_lambda(value) return self.validator(value) - async def process(self, value, args=()): + async def process( + self, value: Any, args: list[tuple[SafeExpType, str]] | None = None + ) -> Expression: if value is None: return None if isinstance(value, Lambda): + # Local import to avoid circular import + from .lvcode import CodeContext, LambdaContext + + if TYPE_CHECKING: + # CodeContext does not have get_automation_parameters + # so we need to assert the type here + assert isinstance(CodeContext.code_context, LambdaContext) + args = args or CodeContext.code_context.get_automation_parameters() return cg.RawExpression( call_lambda( await cg.process_lambda(value, args, return_type=self.rtype) diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index d345ac70f3..6f95a32a18 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -1,3 +1,5 @@ +from typing import TYPE_CHECKING, Any + import esphome.codegen as cg from esphome.components import image from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw @@ -17,6 +19,7 @@ from esphome.cpp_generator import MockObj from esphome.cpp_types import ESPTime, int32, uint32 from esphome.helpers import cpp_string_escape from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor +from esphome.types import Expression, SafeExpType from . import types as ty from .defines import ( @@ -388,11 +391,23 @@ class TextValidator(LValidator): return value return super().__call__(value) - async def process(self, value, args=()): + async def process( + self, value: Any, args: list[tuple[SafeExpType, str]] | None = None + ) -> Expression: + # Local import to avoid circular import at module level + + from .lvcode import CodeContext, LambdaContext + + if TYPE_CHECKING: + # CodeContext does not have get_automation_parameters + # so we need to assert the type here + assert isinstance(CodeContext.code_context, LambdaContext) + args = args or CodeContext.code_context.get_automation_parameters() + if isinstance(value, dict): if format_str := value.get(CONF_FORMAT): - args = [str(x) for x in value[CONF_ARGS]] - arg_expr = cg.RawExpression(",".join(args)) + str_args = [str(x) for x in value[CONF_ARGS]] + arg_expr = cg.RawExpression(",".join(str_args)) format_str = cpp_string_escape(format_str) return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") if time_format := value.get(CONF_TIME_FORMAT): diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index 7a5c35f896..ea38845c07 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -164,6 +164,9 @@ class LambdaContext(CodeContext): code_text.append(text) return code_text + def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]: + return self.parameters + async def __aenter__(self): await super().__aenter__() add_line_marks(self.where) @@ -178,9 +181,8 @@ class LvContext(LambdaContext): added_lambda_count = 0 - def __init__(self, args=None): - self.args = args or LVGL_COMP_ARG - super().__init__(parameters=self.args) + def __init__(self): + super().__init__(parameters=LVGL_COMP_ARG) async def __aexit__(self, exc_type, exc_val, exc_tb): await super().__aexit__(exc_type, exc_val, exc_tb) @@ -189,6 +191,11 @@ class LvContext(LambdaContext): cg.add(expression) return expression + def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]: + # When generating automations, we don't want the `lv_component` parameter to be passed + # to the lambda. + return [] + def __call__(self, *args): return self.add(*args) diff --git a/esphome/components/lvgl/sensor/__init__.py b/esphome/components/lvgl/sensor/__init__.py index 03b2638ed0..167af9c6e1 100644 --- a/esphome/components/lvgl/sensor/__init__.py +++ b/esphome/components/lvgl/sensor/__init__.py @@ -5,7 +5,6 @@ from ..defines import CONF_WIDGET from ..lvcode import ( API_EVENT, EVENT_ARG, - LVGL_COMP_ARG, UPDATE_EVENT, LambdaContext, LvContext, @@ -30,7 +29,7 @@ async def to_code(config): await wait_for_widgets() async with LambdaContext(EVENT_ARG) as lamb: lv_add(sensor.publish_state(widget.get_value())) - async with LvContext(LVGL_COMP_ARG): + async with LvContext(): lv_add( lvgl_static.add_event_cb( widget.obj, diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index d9b7013a1e..c70dd7568d 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -52,6 +52,19 @@ number: widget: spinbox_id id: lvgl_spinbox_number name: LVGL Spinbox Number + - platform: template + id: test_brightness + name: "Test Brightness" + min_value: 0 + max_value: 255 + step: 1 + optimistic: true + # Test lambda in automation accessing x parameter directly + # This is a real-world pattern from user configs + on_value: + - lambda: !lambda |- + // Direct use of x parameter in automation + ESP_LOGD("test", "Brightness: %.0f", x); light: - platform: lvgl @@ -110,3 +123,21 @@ text: platform: lvgl widget: hello_label mode: text + +text_sensor: + - platform: template + id: test_text_sensor + name: "Test Text Sensor" + # Test nested lambdas in LVGL actions can access automation parameters + on_value: + - lvgl.label.update: + id: hello_label + text: !lambda return x.c_str(); + - lvgl.label.update: + id: hello_label + text: !lambda |- + // Test complex lambda with conditionals accessing x parameter + if (x == "*") { + return "WILDCARD"; + } + return x.c_str(); diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 582531e943..14241a1669 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -257,7 +257,30 @@ lvgl: text: "Hello shiny day" text_color: 0xFFFFFF align: bottom_mid + - label: + id: setup_lambda_label + # Test lambda in widget property during setup (LvContext) + # Should NOT receive lv_component parameter + text: !lambda |- + char buf[32]; + snprintf(buf, sizeof(buf), "Setup: %d", 42); + return std::string(buf); + align: top_mid text_font: space16 + - label: + id: chip_info_label + # Test complex setup lambda (real-world pattern) + # Should NOT receive lv_component parameter + text: !lambda |- + // Test conditional compilation and string formatting + char buf[64]; + #ifdef USE_ESP_IDF + snprintf(buf, sizeof(buf), "IDF: v%d.%d", ESP_IDF_VERSION_MAJOR, ESP_IDF_VERSION_MINOR); + #else + snprintf(buf, sizeof(buf), "Arduino"); + #endif + return std::string(buf); + align: top_left - obj: align: center arc_opa: COVER From 6fb490f49b35d4610677e2f91b2230f5e5a012c4 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:40:22 -0400 Subject: [PATCH 210/394] [remote_transmitter] Add non-blocking mode (#11524) --- .../components/remote_transmitter/__init__.py | 20 +++++++++++++ .../remote_transmitter/remote_transmitter.h | 3 ++ .../remote_transmitter_esp32.cpp | 30 +++++++++++++++---- .../remote_transmitter/esp32-common.yaml | 1 + 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index cb98c017f1..faa6c827f7 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -1,3 +1,5 @@ +import logging + from esphome import automation, pins import esphome.codegen as cg from esphome.components import esp32, esp32_rmt, remote_base @@ -18,9 +20,12 @@ from esphome.const import ( ) from esphome.core import CORE +_LOGGER = logging.getLogger(__name__) + AUTO_LOAD = ["remote_base"] CONF_EOT_LEVEL = "eot_level" +CONF_NON_BLOCKING = "non_blocking" CONF_ON_TRANSMIT = "on_transmit" CONF_ON_COMPLETE = "on_complete" CONF_TRANSMITTER_ID = remote_base.CONF_TRANSMITTER_ID @@ -65,11 +70,25 @@ CONFIG_SCHEMA = cv.Schema( esp32_c6=48, esp32_h2=48, ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), + cv.Optional(CONF_NON_BLOCKING): cv.All(cv.only_on_esp32, cv.boolean), cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True), cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True), } ).extend(cv.COMPONENT_SCHEMA) + +def _validate_non_blocking(config): + if CORE.is_esp32 and CONF_NON_BLOCKING not in config: + _LOGGER.warning( + "'non_blocking' is not set for 'remote_transmitter' and will default to 'true'.\n" + "The default behavior changed in 2025.11.0; previously blocking mode was used.\n" + "To silence this warning, explicitly set 'non_blocking: true' (or 'false')." + ) + config[CONF_NON_BLOCKING] = True + + +FINAL_VALIDATE_SCHEMA = _validate_non_blocking + DIGITAL_WRITE_ACTION_SCHEMA = cv.maybe_simple_value( { cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterComponent), @@ -95,6 +114,7 @@ async def to_code(config): if CORE.is_esp32: var = cg.new_Pvariable(config[CONF_ID], pin) cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) + cg.add(var.set_non_blocking(config[CONF_NON_BLOCKING])) if CONF_CLOCK_RESOLUTION in config: cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION])) if CONF_USE_DMA in config: diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index b5d8e8d83f..cc3b82ad61 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -54,6 +54,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, #if defined(USE_ESP32) void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; } + void set_non_blocking(bool non_blocking) { this->non_blocking_ = non_blocking; } #endif Trigger<> *get_transmit_trigger() const { return this->transmit_trigger_; }; @@ -74,6 +75,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, #ifdef USE_ESP32 void configure_rmt_(); + void wait_for_rmt_(); #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) RemoteTransmitterComponentStore store_{}; @@ -90,6 +92,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, esp_err_t error_code_{ESP_OK}; std::string error_string_{""}; bool inverted_{false}; + bool non_blocking_{false}; #endif uint8_t carrier_duty_percent_; diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp index 27bbf3c210..59c85c99a8 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp @@ -196,12 +196,29 @@ void RemoteTransmitterComponent::configure_rmt_() { } } +void RemoteTransmitterComponent::wait_for_rmt_() { + esp_err_t error = rmt_tx_wait_all_done(this->channel_, -1); + if (error != ESP_OK) { + ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error)); + this->status_set_warning(); + } + + this->complete_trigger_->trigger(); +} + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { + uint64_t total_duration = 0; + if (this->is_failed()) { return; } + // if the timeout was cancelled, block until the tx is complete + if (this->non_blocking_ && this->cancel_timeout("complete")) { + this->wait_for_rmt_(); + } + if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) { this->current_carrier_frequency_ = this->temp_.get_carrier_frequency(); this->configure_rmt_(); @@ -212,6 +229,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen // encode any delay at the start of the buffer to simplify the encoder callback // this will be skipped the first time around + total_duration += send_wait * (send_times - 1); send_wait = this->from_microseconds_(static_cast(send_wait)); while (send_wait > 0) { int32_t duration = std::min(send_wait, uint32_t(RMT_SYMBOL_DURATION_MAX)); @@ -229,6 +247,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen if (!level) { value = -value; } + total_duration += value * send_times; value = this->from_microseconds_(static_cast(value)); while (value > 0) { int32_t duration = std::min(value, int32_t(RMT_SYMBOL_DURATION_MAX)); @@ -260,13 +279,12 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen } else { this->status_clear_warning(); } - error = rmt_tx_wait_all_done(this->channel_, -1); - if (error != ESP_OK) { - ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error)); - this->status_set_warning(); - } - this->complete_trigger_->trigger(); + if (this->non_blocking_) { + this->set_timeout("complete", total_duration / 1000, [this]() { this->wait_for_rmt_(); }); + } else { + this->wait_for_rmt_(); + } } #else void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { diff --git a/tests/components/remote_transmitter/esp32-common.yaml b/tests/components/remote_transmitter/esp32-common.yaml index 8b26c45149..79fd47ae21 100644 --- a/tests/components/remote_transmitter/esp32-common.yaml +++ b/tests/components/remote_transmitter/esp32-common.yaml @@ -2,6 +2,7 @@ remote_transmitter: - id: xmitr pin: ${pin} carrier_duty_percent: 50% + non_blocking: true clock_resolution: ${clock_resolution} rmt_symbols: ${rmt_symbols} From f18c70a256328ecb9037ad45ff405a67f796565c Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Wed, 29 Oct 2025 19:06:55 +0100 Subject: [PATCH 211/394] [core] Fix substitution id redefinition false positive (#11603) --- esphome/config.py | 3 +++ tests/unit_tests/core/test_config.py | 11 +++++++++++ .../core/config/id_collision_with_substitution.yaml | 12 ++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 tests/unit_tests/fixtures/core/config/id_collision_with_substitution.yaml diff --git a/esphome/config.py b/esphome/config.py index 634dba8dad..e508ca585b 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -319,6 +319,9 @@ def iter_ids(config, path=None): yield from iter_ids(item, path + [i]) elif isinstance(config, dict): for key, value in config.items(): + if len(path) == 0 and key == CONF_SUBSTITUTIONS: + # Ignore IDs in substitution definitions. + continue if isinstance(key, core.ID): yield key, path yield from iter_ids(value, path + [key]) diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index a1e4627dc9..90b2f5edba 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -261,6 +261,17 @@ def test_device_duplicate_id( assert "ID duplicate_device redefined!" in captured.out +def test_substitution_with_id( + yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] +) -> None: + """Test that a ids coming from substitutions do not cause false positive ID redefinition.""" + load_config_from_fixture( + yaml_file, "id_collision_with_substitution.yaml", FIXTURES_DIR + ) + captured = capsys.readouterr() + assert "ID some_switch_id redefined!" not in captured.out + + def test_add_platform_defines_priority() -> None: """Test that _add_platform_defines runs after globals. diff --git a/tests/unit_tests/fixtures/core/config/id_collision_with_substitution.yaml b/tests/unit_tests/fixtures/core/config/id_collision_with_substitution.yaml new file mode 100644 index 0000000000..840d9ac925 --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/id_collision_with_substitution.yaml @@ -0,0 +1,12 @@ +esphome: + name: test + +host: + +substitutions: + support_switches: + - platform: gpio + id: some_switch_id + pin: 12 + +switch: $support_switches From b6516c687dfcb525ff1cadedbcfd6484288ce369 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 14:21:34 -0500 Subject: [PATCH 212/394] fix template regression --- .../binary_sensor/template_binary_sensor.cpp | 12 +- .../binary_sensor/template_binary_sensor.h | 8 +- .../template/cover/template_cover.cpp | 33 +++-- .../template/cover/template_cover.h | 15 ++- .../template/datetime/template_date.cpp | 18 +-- .../template/datetime/template_date.h | 5 +- .../template/datetime/template_datetime.cpp | 24 ++-- .../template/datetime/template_datetime.h | 5 +- .../template/datetime/template_time.cpp | 18 +-- .../template/datetime/template_time.h | 5 +- .../template/lock/template_lock.cpp | 12 +- .../components/template/lock/template_lock.h | 8 +- .../template/number/template_number.cpp | 12 +- .../template/number/template_number.h | 5 +- .../template/select/template_select.cpp | 19 +-- .../template/select/template_select.h | 5 +- .../template/sensor/template_sensor.cpp | 8 +- .../template/sensor/template_sensor.h | 5 +- .../template/switch/template_switch.cpp | 15 +-- .../template/switch/template_switch.h | 8 +- .../template/text/template_text.cpp | 21 +-- .../components/template/text/template_text.h | 5 +- .../text_sensor/template_text_sensor.cpp | 8 +- .../text_sensor/template_text_sensor.h | 5 +- .../template/valve/template_valve.cpp | 17 ++- .../template/valve/template_valve.h | 8 +- esphome/core/template_lambda.h | 120 ++++++++++++++++++ 27 files changed, 267 insertions(+), 157 deletions(-) create mode 100644 esphome/core/template_lambda.h diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.cpp b/esphome/components/template/binary_sensor/template_binary_sensor.cpp index 8543dff4dc..25879f876d 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.cpp +++ b/esphome/components/template/binary_sensor/template_binary_sensor.cpp @@ -6,17 +6,19 @@ namespace template_ { static const char *const TAG = "template.binary_sensor"; -void TemplateBinarySensor::setup() { this->loop(); } +void TemplateBinarySensor::setup() { + if (!this->f_.has_value()) + this->disable_loop(); + this->loop(); +} void TemplateBinarySensor::loop() { - if (!this->f_.has_value()) - return; - - auto s = (*this->f_)(); + auto s = this->f_(); if (s.has_value()) { this->publish_state(*s); } } + void TemplateBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Template Binary Sensor", this); } } // namespace template_ diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.h b/esphome/components/template/binary_sensor/template_binary_sensor.h index 2e0b216eb4..0373f898a8 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.h +++ b/esphome/components/template/binary_sensor/template_binary_sensor.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/binary_sensor/binary_sensor.h" namespace esphome { @@ -8,7 +9,10 @@ namespace template_ { class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor { public: - void set_template(optional (*f)()) { this->f_ = f; } + template void set_template(F &&f) { + this->f_.set(std::forward(f)); + this->enable_loop(); + } void setup() override; void loop() override; @@ -17,7 +21,7 @@ class TemplateBinarySensor : public Component, public binary_sensor::BinarySenso float get_setup_priority() const override { return setup_priority::HARDWARE; } protected: - optional (*)()> f_; + TemplateLambda f_; }; } // namespace template_ diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index bed3931e78..a87f28ccec 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -33,28 +33,27 @@ void TemplateCover::setup() { break; } } + if (!this->state_f_.has_value() && !this->tilt_f_.has_value()) + this->disable_loop(); } void TemplateCover::loop() { bool changed = false; - if (this->state_f_.has_value()) { - auto s = (*this->state_f_)(); - if (s.has_value()) { - auto pos = clamp(*s, 0.0f, 1.0f); - if (pos != this->position) { - this->position = pos; - changed = true; - } + auto s = this->state_f_(); + if (s.has_value()) { + auto pos = clamp(*s, 0.0f, 1.0f); + if (pos != this->position) { + this->position = pos; + changed = true; } } - if (this->tilt_f_.has_value()) { - auto s = (*this->tilt_f_)(); - if (s.has_value()) { - auto tilt = clamp(*s, 0.0f, 1.0f); - if (tilt != this->tilt) { - this->tilt = tilt; - changed = true; - } + + auto tilt = this->tilt_f_(); + if (tilt.has_value()) { + auto tilt_val = clamp(*tilt, 0.0f, 1.0f); + if (tilt_val != this->tilt) { + this->tilt = tilt_val; + changed = true; } } @@ -63,7 +62,6 @@ void TemplateCover::loop() { } void TemplateCover::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void TemplateCover::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } -void TemplateCover::set_state_lambda(optional (*f)()) { this->state_f_ = f; } float TemplateCover::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; } Trigger<> *TemplateCover::get_close_trigger() const { return this->close_trigger_; } @@ -124,7 +122,6 @@ CoverTraits TemplateCover::get_traits() { } Trigger *TemplateCover::get_position_trigger() const { return this->position_trigger_; } Trigger *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; } -void TemplateCover::set_tilt_lambda(optional (*tilt_f)()) { this->tilt_f_ = tilt_f; } void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } void TemplateCover::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index ed1ebf4e43..56ab61c3fb 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/cover/cover.h" namespace esphome { @@ -17,7 +18,14 @@ class TemplateCover : public cover::Cover, public Component { public: TemplateCover(); - void set_state_lambda(optional (*f)()); + template void set_state_lambda(F &&f) { + this->state_f_.set(std::forward(f)); + this->enable_loop(); + } + template void set_tilt_lambda(F &&f) { + this->tilt_f_.set(std::forward(f)); + this->enable_loop(); + } Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; @@ -26,7 +34,6 @@ class TemplateCover : public cover::Cover, public Component { Trigger *get_tilt_trigger() const; void set_optimistic(bool optimistic); void set_assumed_state(bool assumed_state); - void set_tilt_lambda(optional (*tilt_f)()); void set_has_stop(bool has_stop); void set_has_position(bool has_position); void set_has_tilt(bool has_tilt); @@ -45,8 +52,8 @@ class TemplateCover : public cover::Cover, public Component { void stop_prev_trigger_(); TemplateCoverRestoreMode restore_mode_{COVER_RESTORE}; - optional (*)()> state_f_; - optional (*)()> tilt_f_; + TemplateLambda state_f_; + TemplateLambda tilt_f_; bool assumed_state_{false}; bool optimistic_{false}; Trigger<> *open_trigger_; diff --git a/esphome/components/template/datetime/template_date.cpp b/esphome/components/template/datetime/template_date.cpp index 2fa8016802..40d0e2729a 100644 --- a/esphome/components/template/datetime/template_date.cpp +++ b/esphome/components/template/datetime/template_date.cpp @@ -37,17 +37,13 @@ void TemplateDate::setup() { } void TemplateDate::update() { - if (!this->f_.has_value()) - return; - - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - this->year_ = val->year; - this->month_ = val->month; - this->day_ = val->day_of_month; - this->publish_state(); + auto val = this->f_(); + if (val.has_value()) { + this->year_ = val->year; + this->month_ = val->month; + this->day_ = val->day_of_month; + this->publish_state(); + } } void TemplateDate::control(const datetime::DateCall &call) { diff --git a/esphome/components/template/datetime/template_date.h b/esphome/components/template/datetime/template_date.h index 2a0967fc94..7fed704d0e 100644 --- a/esphome/components/template/datetime/template_date.h +++ b/esphome/components/template/datetime/template_date.h @@ -9,13 +9,14 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/time.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { class TemplateDate : public datetime::DateEntity, public PollingComponent { public: - void set_template(optional (*f)()) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void update() override; @@ -35,7 +36,7 @@ class TemplateDate : public datetime::DateEntity, public PollingComponent { ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional (*)()> f_; + TemplateLambda f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/datetime/template_datetime.cpp b/esphome/components/template/datetime/template_datetime.cpp index a4a4e47d65..acf6dd5ea4 100644 --- a/esphome/components/template/datetime/template_datetime.cpp +++ b/esphome/components/template/datetime/template_datetime.cpp @@ -40,20 +40,16 @@ void TemplateDateTime::setup() { } void TemplateDateTime::update() { - if (!this->f_.has_value()) - return; - - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - this->year_ = val->year; - this->month_ = val->month; - this->day_ = val->day_of_month; - this->hour_ = val->hour; - this->minute_ = val->minute; - this->second_ = val->second; - this->publish_state(); + auto val = this->f_(); + if (val.has_value()) { + this->year_ = val->year; + this->month_ = val->month; + this->day_ = val->day_of_month; + this->hour_ = val->hour; + this->minute_ = val->minute; + this->second_ = val->second; + this->publish_state(); + } } void TemplateDateTime::control(const datetime::DateTimeCall &call) { diff --git a/esphome/components/template/datetime/template_datetime.h b/esphome/components/template/datetime/template_datetime.h index d917015b67..ec45bf0326 100644 --- a/esphome/components/template/datetime/template_datetime.h +++ b/esphome/components/template/datetime/template_datetime.h @@ -9,13 +9,14 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/time.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent { public: - void set_template(optional (*f)()) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void update() override; @@ -35,7 +36,7 @@ class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponen ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional (*)()> f_; + TemplateLambda f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/datetime/template_time.cpp b/esphome/components/template/datetime/template_time.cpp index 349700f187..b27d6fc414 100644 --- a/esphome/components/template/datetime/template_time.cpp +++ b/esphome/components/template/datetime/template_time.cpp @@ -37,17 +37,13 @@ void TemplateTime::setup() { } void TemplateTime::update() { - if (!this->f_.has_value()) - return; - - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - this->hour_ = val->hour; - this->minute_ = val->minute; - this->second_ = val->second; - this->publish_state(); + auto val = this->f_(); + if (val.has_value()) { + this->hour_ = val->hour; + this->minute_ = val->minute; + this->second_ = val->second; + this->publish_state(); + } } void TemplateTime::control(const datetime::TimeCall &call) { diff --git a/esphome/components/template/datetime/template_time.h b/esphome/components/template/datetime/template_time.h index 2f05ba0737..ea7474c0ba 100644 --- a/esphome/components/template/datetime/template_time.h +++ b/esphome/components/template/datetime/template_time.h @@ -9,13 +9,14 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/time.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { class TemplateTime : public datetime::TimeEntity, public PollingComponent { public: - void set_template(optional (*f)()) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void update() override; @@ -35,7 +36,7 @@ class TemplateTime : public datetime::TimeEntity, public PollingComponent { ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional (*)()> f_; + TemplateLambda f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/lock/template_lock.cpp b/esphome/components/template/lock/template_lock.cpp index c2e227c26d..634924a805 100644 --- a/esphome/components/template/lock/template_lock.cpp +++ b/esphome/components/template/lock/template_lock.cpp @@ -12,13 +12,10 @@ TemplateLock::TemplateLock() : lock_trigger_(new Trigger<>()), unlock_trigger_(new Trigger<>()), open_trigger_(new Trigger<>()) {} void TemplateLock::loop() { - if (!this->f_.has_value()) - return; - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - this->publish_state(*val); + auto val = this->f_(); + if (val.has_value()) { + this->publish_state(*val); + } } void TemplateLock::control(const lock::LockCall &call) { if (this->prev_trigger_ != nullptr) { @@ -45,7 +42,6 @@ void TemplateLock::open_latch() { this->open_trigger_->trigger(); } void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } -void TemplateLock::set_state_lambda(optional (*f)()) { this->f_ = f; } float TemplateLock::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateLock::get_lock_trigger() const { return this->lock_trigger_; } Trigger<> *TemplateLock::get_unlock_trigger() const { return this->unlock_trigger_; } diff --git a/esphome/components/template/lock/template_lock.h b/esphome/components/template/lock/template_lock.h index 428744a66f..347c4effb3 100644 --- a/esphome/components/template/lock/template_lock.h +++ b/esphome/components/template/lock/template_lock.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/lock/lock.h" namespace esphome { @@ -13,7 +14,10 @@ class TemplateLock : public lock::Lock, public Component { void dump_config() override; - void set_state_lambda(optional (*f)()); + template void set_state_lambda(F &&f) { + this->f_.set(std::forward(f)); + this->enable_loop(); + } Trigger<> *get_lock_trigger() const; Trigger<> *get_unlock_trigger() const; Trigger<> *get_open_trigger() const; @@ -26,7 +30,7 @@ class TemplateLock : public lock::Lock, public Component { void control(const lock::LockCall &call) override; void open_latch() override; - optional (*)()> f_; + TemplateLambda f_; bool optimistic_{false}; Trigger<> *lock_trigger_; Trigger<> *unlock_trigger_; diff --git a/esphome/components/template/number/template_number.cpp b/esphome/components/template/number/template_number.cpp index 187f426273..b912dc415d 100644 --- a/esphome/components/template/number/template_number.cpp +++ b/esphome/components/template/number/template_number.cpp @@ -27,14 +27,10 @@ void TemplateNumber::setup() { } void TemplateNumber::update() { - if (!this->f_.has_value()) - return; - - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - this->publish_state(*val); + auto val = this->f_(); + if (val.has_value()) { + this->publish_state(*val); + } } void TemplateNumber::control(float value) { diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h index e77b181d25..a9307e9246 100644 --- a/esphome/components/template/number/template_number.h +++ b/esphome/components/template/number/template_number.h @@ -4,13 +4,14 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { class TemplateNumber : public number::Number, public PollingComponent { public: - void set_template(optional (*f)()) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void update() override; @@ -28,7 +29,7 @@ class TemplateNumber : public number::Number, public PollingComponent { float initial_value_{NAN}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional (*)()> f_; + TemplateLambda f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index c7a1d8a344..053f3a83fd 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -28,19 +28,14 @@ void TemplateSelect::setup() { } void TemplateSelect::update() { - if (!this->f_.has_value()) - return; - - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - if (!this->has_option(*val)) { - ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str()); - return; + auto val = this->f_(); + if (val.has_value()) { + if (!this->has_option(*val)) { + ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str()); + return; + } + this->publish_state(*val); } - - this->publish_state(*val); } void TemplateSelect::control(const std::string &value) { diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index e77e4d8f14..1c33153872 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -4,13 +4,14 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { class TemplateSelect : public select::Select, public PollingComponent { public: - void set_template(optional (*f)()) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void update() override; @@ -28,7 +29,7 @@ class TemplateSelect : public select::Select, public PollingComponent { size_t initial_option_index_{0}; bool restore_value_ = false; Trigger *set_trigger_ = new Trigger(); - optional (*)()> f_; + TemplateLambda f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/sensor/template_sensor.cpp b/esphome/components/template/sensor/template_sensor.cpp index 65f2417670..43dd447a02 100644 --- a/esphome/components/template/sensor/template_sensor.cpp +++ b/esphome/components/template/sensor/template_sensor.cpp @@ -8,16 +8,14 @@ namespace template_ { static const char *const TAG = "template.sensor"; void TemplateSensor::update() { - if (!this->f_.has_value()) - return; - - auto val = (*this->f_)(); + auto val = this->f_(); if (val.has_value()) { this->publish_state(*val); } } + float TemplateSensor::get_setup_priority() const { return setup_priority::HARDWARE; } -void TemplateSensor::set_template(optional (*f)()) { this->f_ = f; } + void TemplateSensor::dump_config() { LOG_SENSOR("", "Template Sensor", this); LOG_UPDATE_INTERVAL(this); diff --git a/esphome/components/template/sensor/template_sensor.h b/esphome/components/template/sensor/template_sensor.h index 369313d607..793d754a0f 100644 --- a/esphome/components/template/sensor/template_sensor.h +++ b/esphome/components/template/sensor/template_sensor.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/sensor/sensor.h" namespace esphome { @@ -8,7 +9,7 @@ namespace template_ { class TemplateSensor : public sensor::Sensor, public PollingComponent { public: - void set_template(optional (*f)()); + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void update() override; @@ -17,7 +18,7 @@ class TemplateSensor : public sensor::Sensor, public PollingComponent { float get_setup_priority() const override; protected: - optional (*)()> f_; + TemplateLambda f_; }; } // namespace template_ diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index 5aaf514b2a..95e8692da5 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -9,13 +9,10 @@ static const char *const TAG = "template.switch"; TemplateSwitch::TemplateSwitch() : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {} void TemplateSwitch::loop() { - if (!this->f_.has_value()) - return; - auto s = (*this->f_)(); - if (!s.has_value()) - return; - - this->publish_state(*s); + auto s = this->f_(); + if (s.has_value()) { + this->publish_state(*s); + } } void TemplateSwitch::write_state(bool state) { if (this->prev_trigger_ != nullptr) { @@ -35,11 +32,13 @@ void TemplateSwitch::write_state(bool state) { } void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } bool TemplateSwitch::assumed_state() { return this->assumed_state_; } -void TemplateSwitch::set_state_lambda(optional (*f)()) { this->f_ = f; } float TemplateSwitch::get_setup_priority() const { return setup_priority::HARDWARE - 2.0f; } Trigger<> *TemplateSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } Trigger<> *TemplateSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } void TemplateSwitch::setup() { + if (!this->f_.has_value()) + this->disable_loop(); + optional initial_state = this->get_initial_state_with_restore_mode(); if (initial_state.has_value()) { diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index 0fba66b9bd..47154fd047 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/switch/switch.h" namespace esphome { @@ -14,7 +15,10 @@ class TemplateSwitch : public switch_::Switch, public Component { void setup() override; void dump_config() override; - void set_state_lambda(optional (*f)()); + template void set_state_lambda(F &&f) { + this->f_.set(std::forward(f)); + this->enable_loop(); + } Trigger<> *get_turn_on_trigger() const; Trigger<> *get_turn_off_trigger() const; void set_optimistic(bool optimistic); @@ -28,7 +32,7 @@ class TemplateSwitch : public switch_::Switch, public Component { void write_state(bool state) override; - optional (*)()> f_; + TemplateLambda f_; bool optimistic_{false}; bool assumed_state_{false}; Trigger<> *turn_on_trigger_; diff --git a/esphome/components/template/text/template_text.cpp b/esphome/components/template/text/template_text.cpp index d8e840ba7e..edef97ae06 100644 --- a/esphome/components/template/text/template_text.cpp +++ b/esphome/components/template/text/template_text.cpp @@ -7,10 +7,8 @@ namespace template_ { static const char *const TAG = "template.text"; void TemplateText::setup() { - if (!(this->f_ == nullptr)) { - if (this->f_.has_value()) - return; - } + if (this->f_.has_value()) + return; std::string value = this->initial_value_; if (!this->pref_) { ESP_LOGD(TAG, "State from initial: %s", value.c_str()); @@ -26,17 +24,10 @@ void TemplateText::setup() { } void TemplateText::update() { - if (this->f_ == nullptr) - return; - - if (!this->f_.has_value()) - return; - - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - this->publish_state(*val); + auto val = this->f_(); + if (val.has_value()) { + this->publish_state(*val); + } } void TemplateText::control(const std::string &value) { diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index 6c17d2016a..c12021f80e 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -4,6 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { @@ -61,7 +62,7 @@ template class TextSaver : public TemplateTextSaverBase { class TemplateText : public text::Text, public PollingComponent { public: - void set_template(optional (*f)()) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void update() override; @@ -78,7 +79,7 @@ class TemplateText : public text::Text, public PollingComponent { bool optimistic_ = false; std::string initial_value_; Trigger *set_trigger_ = new Trigger(); - optional (*)()> f_{nullptr}; + TemplateLambda f_{}; TemplateTextSaverBase *pref_ = nullptr; }; diff --git a/esphome/components/template/text_sensor/template_text_sensor.cpp b/esphome/components/template/text_sensor/template_text_sensor.cpp index 2b0297d62f..7d38e4b0b7 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.cpp +++ b/esphome/components/template/text_sensor/template_text_sensor.cpp @@ -7,16 +7,14 @@ namespace template_ { static const char *const TAG = "template.text_sensor"; void TemplateTextSensor::update() { - if (!this->f_.has_value()) - return; - - auto val = (*this->f_)(); + auto val = this->f_(); if (val.has_value()) { this->publish_state(*val); } } + float TemplateTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } -void TemplateTextSensor::set_template(optional (*f)()) { this->f_ = f; } + void TemplateTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Template Sensor", this); } } // namespace template_ diff --git a/esphome/components/template/text_sensor/template_text_sensor.h b/esphome/components/template/text_sensor/template_text_sensor.h index 48e40c2493..0d01c72023 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.h +++ b/esphome/components/template/text_sensor/template_text_sensor.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/text_sensor/text_sensor.h" namespace esphome { @@ -9,7 +10,7 @@ namespace template_ { class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent { public: - void set_template(optional (*f)()); + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void update() override; @@ -18,7 +19,7 @@ class TemplateTextSensor : public text_sensor::TextSensor, public PollingCompone void dump_config() override; protected: - optional (*)()> f_{}; + TemplateLambda f_{}; }; } // namespace template_ diff --git a/esphome/components/template/valve/template_valve.cpp b/esphome/components/template/valve/template_valve.cpp index b27cc00968..b91b32473e 100644 --- a/esphome/components/template/valve/template_valve.cpp +++ b/esphome/components/template/valve/template_valve.cpp @@ -33,19 +33,19 @@ void TemplateValve::setup() { break; } } + if (!this->state_f_.has_value()) + this->disable_loop(); } void TemplateValve::loop() { bool changed = false; - if (this->state_f_.has_value()) { - auto s = (*this->state_f_)(); - if (s.has_value()) { - auto pos = clamp(*s, 0.0f, 1.0f); - if (pos != this->position) { - this->position = pos; - changed = true; - } + auto s = this->state_f_(); + if (s.has_value()) { + auto pos = clamp(*s, 0.0f, 1.0f); + if (pos != this->position) { + this->position = pos; + changed = true; } } @@ -55,7 +55,6 @@ void TemplateValve::loop() { void TemplateValve::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void TemplateValve::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } -void TemplateValve::set_state_lambda(optional (*f)()) { this->state_f_ = f; } float TemplateValve::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateValve::get_open_trigger() const { return this->open_trigger_; } diff --git a/esphome/components/template/valve/template_valve.h b/esphome/components/template/valve/template_valve.h index 92c32f3487..23a77ff918 100644 --- a/esphome/components/template/valve/template_valve.h +++ b/esphome/components/template/valve/template_valve.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/valve/valve.h" namespace esphome { @@ -17,7 +18,10 @@ class TemplateValve : public valve::Valve, public Component { public: TemplateValve(); - void set_state_lambda(optional (*f)()); + template void set_state_lambda(F &&f) { + this->state_f_.set(std::forward(f)); + this->enable_loop(); + } Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; @@ -42,7 +46,7 @@ class TemplateValve : public valve::Valve, public Component { void stop_prev_trigger_(); TemplateValveRestoreMode restore_mode_{VALVE_NO_RESTORE}; - optional (*)()> state_f_; + TemplateLambda state_f_; bool assumed_state_{false}; bool optimistic_{false}; Trigger<> *open_trigger_; diff --git a/esphome/core/template_lambda.h b/esphome/core/template_lambda.h new file mode 100644 index 0000000000..8e7f71b7b2 --- /dev/null +++ b/esphome/core/template_lambda.h @@ -0,0 +1,120 @@ +#pragma once + +#include +#include +#include "esphome/core/optional.h" + +namespace esphome { + +/** Helper class for template platforms that stores either a stateless lambda (function pointer) + * or a stateful lambda (std::function pointer). + * + * This provides backward compatibility with PR #11555 while maintaining the optimization: + * - Stateless lambdas (no capture) → function pointer (4 bytes on ESP32) + * - Stateful lambdas (with capture) → pointer to std::function (4 bytes on ESP32) + * Total size: enum (1 byte) + union (4 bytes) + padding = 8 bytes (same as PR #11555) + * + * Both lambda types must return optional (as YAML codegen does) to support the pattern: + * return {}; // Don't publish a value + * return 42.0; // Publish this value + * + * operator() returns optional, returning nullopt when no lambda is set (type == NONE). + * This eliminates redundant "is lambda set" checks by reusing optional's discriminator. + * + * @tparam T The return type (e.g., float for TemplateLambda>) + * @tparam Args Optional arguments for the lambda + */ +template class TemplateLambda { + public: + TemplateLambda() : type_(NONE) {} + + // For stateless lambdas: use function pointer + template + requires std::invocable && std::convertible_to < F, optional(*) + (Args...) > void set(F f) { + this->reset(); + this->type_ = STATELESS_LAMBDA; + this->stateless_f_ = f; // Implicit conversion to function pointer + } + + // For stateful lambdas: use std::function pointer + template + requires std::invocable && + (!std::convertible_to (*)(Args...)>) &&std::convertible_to, + optional> void set(F &&f) { + this->reset(); + this->type_ = LAMBDA; + this->f_ = new std::function(Args...)>(std::forward(f)); + } + + ~TemplateLambda() { this->reset(); } + + // Copy constructor + TemplateLambda(const TemplateLambda &) = delete; + TemplateLambda &operator=(const TemplateLambda &) = delete; + + // Move constructor + TemplateLambda(TemplateLambda &&other) noexcept : type_(other.type_) { + if (type_ == LAMBDA) { + this->f_ = other.f_; + other.f_ = nullptr; + } else if (type_ == STATELESS_LAMBDA) { + this->stateless_f_ = other.stateless_f_; + } + other.type_ = NONE; + } + + TemplateLambda &operator=(TemplateLambda &&other) noexcept { + if (this != &other) { + this->reset(); + this->type_ = other.type_; + if (type_ == LAMBDA) { + this->f_ = other.f_; + other.f_ = nullptr; + } else if (type_ == STATELESS_LAMBDA) { + this->stateless_f_ = other.stateless_f_; + } + other.type_ = NONE; + } + return *this; + } + + bool has_value() const { return this->type_ != NONE; } + + // Returns optional, returning nullopt if no lambda is set + optional operator()(Args... args) { + switch (this->type_) { + case STATELESS_LAMBDA: + return this->stateless_f_(args...); // Direct function pointer call + case LAMBDA: + return (*this->f_)(args...); // std::function call via pointer + case NONE: + default: + return nullopt; // No lambda set + } + } + + optional call(Args... args) { return (*this)(args...); } + + protected: + void reset() { + if (this->type_ == LAMBDA) { + delete this->f_; + this->f_ = nullptr; + } + this->type_ = NONE; + } + + enum : uint8_t { + NONE, + STATELESS_LAMBDA, + LAMBDA, + } type_; + + union { + optional (*stateless_f_)(Args...); // Function pointer (4 bytes on ESP32) + std::function(Args...)> *f_; // Pointer to std::function (4 bytes on ESP32) + }; +}; + +} // namespace esphome From 299c937e67bd7efae006bd236b279b7e4dcd65fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 14:24:02 -0500 Subject: [PATCH 213/394] fix template regression --- esphome/components/template/datetime/template_date.cpp | 3 +++ esphome/components/template/datetime/template_datetime.cpp | 3 +++ esphome/components/template/datetime/template_time.cpp | 3 +++ esphome/components/template/number/template_number.cpp | 3 +++ esphome/components/template/select/template_select.cpp | 3 +++ esphome/components/template/sensor/template_sensor.cpp | 3 +++ esphome/components/template/text/template_text.cpp | 3 +++ .../components/template/text_sensor/template_text_sensor.cpp | 3 +++ 8 files changed, 24 insertions(+) diff --git a/esphome/components/template/datetime/template_date.cpp b/esphome/components/template/datetime/template_date.cpp index 40d0e2729a..3f6626e847 100644 --- a/esphome/components/template/datetime/template_date.cpp +++ b/esphome/components/template/datetime/template_date.cpp @@ -37,6 +37,9 @@ void TemplateDate::setup() { } void TemplateDate::update() { + if (!this->f_.has_value()) + return; + auto val = this->f_(); if (val.has_value()) { this->year_ = val->year; diff --git a/esphome/components/template/datetime/template_datetime.cpp b/esphome/components/template/datetime/template_datetime.cpp index acf6dd5ea4..62f842a7ad 100644 --- a/esphome/components/template/datetime/template_datetime.cpp +++ b/esphome/components/template/datetime/template_datetime.cpp @@ -40,6 +40,9 @@ void TemplateDateTime::setup() { } void TemplateDateTime::update() { + if (!this->f_.has_value()) + return; + auto val = this->f_(); if (val.has_value()) { this->year_ = val->year; diff --git a/esphome/components/template/datetime/template_time.cpp b/esphome/components/template/datetime/template_time.cpp index b27d6fc414..dab28d01cc 100644 --- a/esphome/components/template/datetime/template_time.cpp +++ b/esphome/components/template/datetime/template_time.cpp @@ -37,6 +37,9 @@ void TemplateTime::setup() { } void TemplateTime::update() { + if (!this->f_.has_value()) + return; + auto val = this->f_(); if (val.has_value()) { this->hour_ = val->hour; diff --git a/esphome/components/template/number/template_number.cpp b/esphome/components/template/number/template_number.cpp index b912dc415d..145a89a2f7 100644 --- a/esphome/components/template/number/template_number.cpp +++ b/esphome/components/template/number/template_number.cpp @@ -27,6 +27,9 @@ void TemplateNumber::setup() { } void TemplateNumber::update() { + if (!this->f_.has_value()) + return; + auto val = this->f_(); if (val.has_value()) { this->publish_state(*val); diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 053f3a83fd..3ea34c3c7c 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -28,6 +28,9 @@ void TemplateSelect::setup() { } void TemplateSelect::update() { + if (!this->f_.has_value()) + return; + auto val = this->f_(); if (val.has_value()) { if (!this->has_option(*val)) { diff --git a/esphome/components/template/sensor/template_sensor.cpp b/esphome/components/template/sensor/template_sensor.cpp index 43dd447a02..1558ea9b15 100644 --- a/esphome/components/template/sensor/template_sensor.cpp +++ b/esphome/components/template/sensor/template_sensor.cpp @@ -8,6 +8,9 @@ namespace template_ { static const char *const TAG = "template.sensor"; void TemplateSensor::update() { + if (!this->f_.has_value()) + return; + auto val = this->f_(); if (val.has_value()) { this->publish_state(*val); diff --git a/esphome/components/template/text/template_text.cpp b/esphome/components/template/text/template_text.cpp index edef97ae06..a917c72a14 100644 --- a/esphome/components/template/text/template_text.cpp +++ b/esphome/components/template/text/template_text.cpp @@ -24,6 +24,9 @@ void TemplateText::setup() { } void TemplateText::update() { + if (!this->f_.has_value()) + return; + auto val = this->f_(); if (val.has_value()) { this->publish_state(*val); diff --git a/esphome/components/template/text_sensor/template_text_sensor.cpp b/esphome/components/template/text_sensor/template_text_sensor.cpp index 7d38e4b0b7..024d0093a2 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.cpp +++ b/esphome/components/template/text_sensor/template_text_sensor.cpp @@ -7,6 +7,9 @@ namespace template_ { static const char *const TAG = "template.text_sensor"; void TemplateTextSensor::update() { + if (!this->f_.has_value()) + return; + auto val = this->f_(); if (val.has_value()) { this->publish_state(*val); From c38a558df833bc6197de3a9613406a417bfdf036 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 14:26:33 -0500 Subject: [PATCH 214/394] fix template regression --- .../template/binary_sensor/template_binary_sensor.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.cpp b/esphome/components/template/binary_sensor/template_binary_sensor.cpp index 25879f876d..806aed49b1 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.cpp +++ b/esphome/components/template/binary_sensor/template_binary_sensor.cpp @@ -7,9 +7,11 @@ namespace template_ { static const char *const TAG = "template.binary_sensor"; void TemplateBinarySensor::setup() { - if (!this->f_.has_value()) + if (!this->f_.has_value()) { this->disable_loop(); - this->loop(); + } else { + this->loop(); + } } void TemplateBinarySensor::loop() { From 399b86255a4c0851d0913aef81ec2f8ce96a1a26 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 14:35:03 -0500 Subject: [PATCH 215/394] [template] Add regression tests for lambdas with captures (PR #11555) --- tests/components/template/common-base.yaml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index b873af5207..7d2cb19077 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -9,6 +9,27 @@ esphome: id: template_sens state: !lambda "return 42.0;" + # Test C++ API: set_template() with stateless lambda (no captures) + - lambda: |- + id(template_sens).set_template([]() -> esphome::optional { + return 123.0f; + }); + + # Test C++ API: set_template() with stateful lambda (with captures) + # This is the regression test for issue #11555 + - lambda: |- + float captured_value = 456.0f; + id(template_sens).set_template([captured_value]() -> esphome::optional { + return captured_value; + }); + + # Test C++ API: set_template() with more complex capture + - lambda: |- + auto sensor_id = id(template_sens); + id(template_number).set_template([sensor_id]() -> esphome::optional { + return sensor_id->state * 2.0f; + }); + - datetime.date.set: id: test_date date: @@ -215,6 +236,7 @@ cover: number: - platform: template + id: template_number name: "Template number" optimistic: true min_value: 0 From d6c23ac0563acfba946fadde1a0183bd23bd0aa1 Mon Sep 17 00:00:00 2001 From: clydebarrow <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 30 Oct 2025 05:38:16 +1000 Subject: [PATCH 216/394] Add clarifying comment --- esphome/components/usb_uart/usb_uart.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 86d4f4078e..c24fffb11d 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -364,6 +364,7 @@ void USBUartTypeCdcAcm::on_disconnected() { usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress); } usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number); + // Reset the input and output started flags to their initial state to avoid the possibility of spurious restarts channel->input_started_.store(true); channel->output_started_.store(true); channel->input_buffer_.clear(); From 658c50e0c621391a9ace28369cbf70f185745f3b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 14:45:50 -0500 Subject: [PATCH 217/394] remove tests to get baseline --- tests/components/template/common-base.yaml | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index 7d2cb19077..b873af5207 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -9,27 +9,6 @@ esphome: id: template_sens state: !lambda "return 42.0;" - # Test C++ API: set_template() with stateless lambda (no captures) - - lambda: |- - id(template_sens).set_template([]() -> esphome::optional { - return 123.0f; - }); - - # Test C++ API: set_template() with stateful lambda (with captures) - # This is the regression test for issue #11555 - - lambda: |- - float captured_value = 456.0f; - id(template_sens).set_template([captured_value]() -> esphome::optional { - return captured_value; - }); - - # Test C++ API: set_template() with more complex capture - - lambda: |- - auto sensor_id = id(template_sens); - id(template_number).set_template([sensor_id]() -> esphome::optional { - return sensor_id->state * 2.0f; - }); - - datetime.date.set: id: test_date date: @@ -236,7 +215,6 @@ cover: number: - platform: template - id: template_number name: "Template number" optimistic: true min_value: 0 From b30c4e716f16713abfd5b79ce10e7eca2b3a58a1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 14:55:15 -0500 Subject: [PATCH 218/394] Revert "remove tests to get baseline" This reverts commit 658c50e0c621391a9ace28369cbf70f185745f3b. --- tests/components/template/common-base.yaml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index b873af5207..7d2cb19077 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -9,6 +9,27 @@ esphome: id: template_sens state: !lambda "return 42.0;" + # Test C++ API: set_template() with stateless lambda (no captures) + - lambda: |- + id(template_sens).set_template([]() -> esphome::optional { + return 123.0f; + }); + + # Test C++ API: set_template() with stateful lambda (with captures) + # This is the regression test for issue #11555 + - lambda: |- + float captured_value = 456.0f; + id(template_sens).set_template([captured_value]() -> esphome::optional { + return captured_value; + }); + + # Test C++ API: set_template() with more complex capture + - lambda: |- + auto sensor_id = id(template_sens); + id(template_number).set_template([sensor_id]() -> esphome::optional { + return sensor_id->state * 2.0f; + }); + - datetime.date.set: id: test_date date: @@ -215,6 +236,7 @@ cover: number: - platform: template + id: template_number name: "Template number" optimistic: true min_value: 0 From a21057a744cbdbc8318afd9de1f7c59b97c18de3 Mon Sep 17 00:00:00 2001 From: clydebarrow <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 30 Oct 2025 06:04:33 +1000 Subject: [PATCH 219/394] Relax memory order to acquire --- esphome/components/usb_host/usb_host_client.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 2456b0c742..dc216a209d 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -348,7 +348,7 @@ TransferRequest *USBClient::get_trq_() { // Slot i appears available, try to claim it atomically trq_bitmask_t desired = mask | lsb; - if (this->trq_in_use_.compare_exchange_weak(mask, desired)) { + if (this->trq_in_use_.compare_exchange_weak(mask, desired, std::memory_order::acquire)) { auto i = __builtin_ctz(lsb); // count trailing zeroes // Successfully claimed slot i - prepare the TransferRequest auto *trq = &this->requests_[i]; From 3636ab68f3d11cfa2a28546310e714dba38b6045 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 15:06:08 -0500 Subject: [PATCH 220/394] tidy --- esphome/core/template_lambda.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/core/template_lambda.h b/esphome/core/template_lambda.h index 8e7f71b7b2..1977265c33 100644 --- a/esphome/core/template_lambda.h +++ b/esphome/core/template_lambda.h @@ -32,7 +32,7 @@ template class TemplateLambda { template requires std::invocable && std::convertible_to < F, optional(*) (Args...) > void set(F f) { - this->reset(); + this->reset_(); this->type_ = STATELESS_LAMBDA; this->stateless_f_ = f; // Implicit conversion to function pointer } @@ -42,12 +42,12 @@ template class TemplateLambda { requires std::invocable && (!std::convertible_to (*)(Args...)>) &&std::convertible_to, optional> void set(F &&f) { - this->reset(); + this->reset_(); this->type_ = LAMBDA; this->f_ = new std::function(Args...)>(std::forward(f)); } - ~TemplateLambda() { this->reset(); } + ~TemplateLambda() { this->reset_(); } // Copy constructor TemplateLambda(const TemplateLambda &) = delete; @@ -66,7 +66,7 @@ template class TemplateLambda { TemplateLambda &operator=(TemplateLambda &&other) noexcept { if (this != &other) { - this->reset(); + this->reset_(); this->type_ = other.type_; if (type_ == LAMBDA) { this->f_ = other.f_; @@ -97,7 +97,7 @@ template class TemplateLambda { optional call(Args... args) { return (*this)(args...); } protected: - void reset() { + void reset_() { if (this->type_ == LAMBDA) { delete this->f_; this->f_ = nullptr; From 287f65cbaf4ab31fcab7a0502ff5ddbeee51d0e4 Mon Sep 17 00:00:00 2001 From: Stuart Parmenter Date: Wed, 29 Oct 2025 13:27:31 -0700 Subject: [PATCH 221/394] [lvgl] fix typo from previous refactor (#11596) --- esphome/components/image/image.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/image/image.cpp b/esphome/components/image/image.cpp index 7b65c4d0cb..90e021467f 100644 --- a/esphome/components/image/image.cpp +++ b/esphome/components/image/image.cpp @@ -125,7 +125,7 @@ lv_img_dsc_t *Image::get_lv_img_dsc() { case IMAGE_TYPE_RGB: #if LV_COLOR_DEPTH == 32 - switch (this->transparent_) { + switch (this->transparency_) { case TRANSPARENCY_ALPHA_CHANNEL: this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA; break; @@ -156,7 +156,8 @@ lv_img_dsc_t *Image::get_lv_img_dsc() { break; } #else - this->dsc_.header.cf = this->transparent_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565; + this->dsc_.header.cf = + this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565; #endif break; } From 918650f15adb10d87ae6d0a4f04accdb27b97b73 Mon Sep 17 00:00:00 2001 From: Stuart Parmenter Date: Wed, 29 Oct 2025 14:06:45 -0700 Subject: [PATCH 222/394] [lvgl] memset canvas buffer to prevent display of random garbage (#11582) Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com> --- esphome/components/lvgl/widgets/canvas.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/esphome/components/lvgl/widgets/canvas.py b/esphome/components/lvgl/widgets/canvas.py index 217e8935f1..f0a9cd35ba 100644 --- a/esphome/components/lvgl/widgets/canvas.py +++ b/esphome/components/lvgl/widgets/canvas.py @@ -33,7 +33,7 @@ from ..lv_validation import ( pixels, size, ) -from ..lvcode import LocalVariable, lv, lv_assign +from ..lvcode import LocalVariable, lv, lv_assign, lv_expr from ..schemas import STYLE_PROPS, STYLE_REMAP, TEXT_SCHEMA, point_schema from ..types import LvType, ObjUpdateAction, WidgetType from . import Widget, get_widgets @@ -70,15 +70,18 @@ class CanvasType(WidgetType): width = config[CONF_WIDTH] height = config[CONF_HEIGHT] use_alpha = "_ALPHA" if config[CONF_TRANSPARENT] else "" - lv.canvas_set_buffer( - w.obj, - lv.custom_mem_alloc( - literal(f"LV_CANVAS_BUF_SIZE_TRUE_COLOR{use_alpha}({width}, {height})") - ), - width, - height, - literal(f"LV_IMG_CF_TRUE_COLOR{use_alpha}"), + buf_size = literal( + f"LV_CANVAS_BUF_SIZE_TRUE_COLOR{use_alpha}({width}, {height})" ) + with LocalVariable("buf", cg.void, lv_expr.custom_mem_alloc(buf_size)) as buf: + cg.add(cg.RawExpression(f"memset({buf}, 0, {buf_size});")) + lv.canvas_set_buffer( + w.obj, + buf, + width, + height, + literal(f"LV_IMG_CF_TRUE_COLOR{use_alpha}"), + ) canvas_spec = CanvasType() From f4d32c7def97506ca93595280a35228f6d977c93 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 16:08:27 -0500 Subject: [PATCH 223/394] relo --- .../binary_sensor/template_binary_sensor.h | 2 +- .../template/cover/template_cover.h | 2 +- .../template/datetime/template_date.h | 2 +- .../template/datetime/template_datetime.h | 2 +- .../template/datetime/template_time.h | 2 +- .../components/template/lock/template_lock.h | 2 +- .../template/number/template_number.h | 2 +- .../template/select/template_select.h | 2 +- .../template/sensor/template_sensor.h | 2 +- .../template/switch/template_switch.h | 2 +- esphome/components/template/template_lambda.h | 51 ++++++++ .../components/template/text/template_text.h | 2 +- .../text_sensor/template_text_sensor.h | 2 +- .../template/valve/template_valve.h | 2 +- esphome/core/template_lambda.h | 120 ------------------ tests/components/template/common-base.yaml | 16 +-- 16 files changed, 65 insertions(+), 148 deletions(-) create mode 100644 esphome/components/template/template_lambda.h delete mode 100644 esphome/core/template_lambda.h diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.h b/esphome/components/template/binary_sensor/template_binary_sensor.h index 0373f898a8..f63738d93f 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.h +++ b/esphome/components/template/binary_sensor/template_binary_sensor.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" #include "esphome/components/binary_sensor/binary_sensor.h" namespace esphome { diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index 56ab61c3fb..57a5e11c5c 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" #include "esphome/components/cover/cover.h" namespace esphome { diff --git a/esphome/components/template/datetime/template_date.h b/esphome/components/template/datetime/template_date.h index 7fed704d0e..b0fdbcbfbb 100644 --- a/esphome/components/template/datetime/template_date.h +++ b/esphome/components/template/datetime/template_date.h @@ -9,7 +9,7 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/time.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" namespace esphome { namespace template_ { diff --git a/esphome/components/template/datetime/template_datetime.h b/esphome/components/template/datetime/template_datetime.h index ec45bf0326..b1c94d7d34 100644 --- a/esphome/components/template/datetime/template_datetime.h +++ b/esphome/components/template/datetime/template_datetime.h @@ -9,7 +9,7 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/time.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" namespace esphome { namespace template_ { diff --git a/esphome/components/template/datetime/template_time.h b/esphome/components/template/datetime/template_time.h index ea7474c0ba..c6938fe7a0 100644 --- a/esphome/components/template/datetime/template_time.h +++ b/esphome/components/template/datetime/template_time.h @@ -9,7 +9,7 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/time.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" namespace esphome { namespace template_ { diff --git a/esphome/components/template/lock/template_lock.h b/esphome/components/template/lock/template_lock.h index 347c4effb3..2bb11bfef6 100644 --- a/esphome/components/template/lock/template_lock.h +++ b/esphome/components/template/lock/template_lock.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" #include "esphome/components/lock/lock.h" namespace esphome { diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h index a9307e9246..1a6e9d964f 100644 --- a/esphome/components/template/number/template_number.h +++ b/esphome/components/template/number/template_number.h @@ -4,7 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" namespace esphome { namespace template_ { diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 1c33153872..53cadfa303 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -4,7 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" namespace esphome { namespace template_ { diff --git a/esphome/components/template/sensor/template_sensor.h b/esphome/components/template/sensor/template_sensor.h index 793d754a0f..27980414f9 100644 --- a/esphome/components/template/sensor/template_sensor.h +++ b/esphome/components/template/sensor/template_sensor.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" #include "esphome/components/sensor/sensor.h" namespace esphome { diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index 47154fd047..f436f657ae 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" #include "esphome/components/switch/switch.h" namespace esphome { diff --git a/esphome/components/template/template_lambda.h b/esphome/components/template/template_lambda.h new file mode 100644 index 0000000000..894b5edffa --- /dev/null +++ b/esphome/components/template/template_lambda.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/core/optional.h" + +namespace esphome { + +/** Lightweight wrapper for template platform lambdas (stateless function pointers only). + * + * This optimizes template platforms by storing only a function pointer (4 bytes on ESP32) + * instead of std::function (16-32 bytes). + * + * IMPORTANT: This only supports stateless lambdas (no captures). The set_template() method + * is an internal API used by YAML codegen, not intended for external use. + * + * Lambdas must return optional to support the pattern: + * return {}; // Don't publish a value + * return 42.0; // Publish this value + * + * operator() returns optional, returning nullopt when no lambda is set (nullptr check). + * + * @tparam T The return type (e.g., float for sensor values) + * @tparam Args Optional arguments for the lambda + */ +template class TemplateLambda { + public: + TemplateLambda() : f_(nullptr) {} + + /** Set the lambda function pointer. + * INTERNAL API: Only for use by YAML codegen. + * Only stateless lambdas (no captures) are supported. + */ + void set(optional (*f)(Args...)) { this->f_ = f; } + + /** Check if a lambda is set */ + bool has_value() const { return this->f_ != nullptr; } + + /** Call the lambda, returning nullopt if no lambda is set */ + optional operator()(Args... args) { + if (this->f_ == nullptr) + return nullopt; + return this->f_(args...); + } + + /** Alias for operator() for compatibility */ + optional call(Args... args) { return (*this)(args...); } + + protected: + optional (*f_)(Args...); // Function pointer (4 bytes on ESP32) +}; + +} // namespace esphome diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index c12021f80e..fd28800bab 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -4,7 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" namespace esphome { namespace template_ { diff --git a/esphome/components/template/text_sensor/template_text_sensor.h b/esphome/components/template/text_sensor/template_text_sensor.h index 0d01c72023..fda28f53c7 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.h +++ b/esphome/components/template/text_sensor/template_text_sensor.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" #include "esphome/components/text_sensor/text_sensor.h" namespace esphome { diff --git a/esphome/components/template/valve/template_valve.h b/esphome/components/template/valve/template_valve.h index 23a77ff918..7713091673 100644 --- a/esphome/components/template/valve/template_valve.h +++ b/esphome/components/template/valve/template_valve.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" #include "esphome/components/valve/valve.h" namespace esphome { diff --git a/esphome/core/template_lambda.h b/esphome/core/template_lambda.h deleted file mode 100644 index 1977265c33..0000000000 --- a/esphome/core/template_lambda.h +++ /dev/null @@ -1,120 +0,0 @@ -#pragma once - -#include -#include -#include "esphome/core/optional.h" - -namespace esphome { - -/** Helper class for template platforms that stores either a stateless lambda (function pointer) - * or a stateful lambda (std::function pointer). - * - * This provides backward compatibility with PR #11555 while maintaining the optimization: - * - Stateless lambdas (no capture) → function pointer (4 bytes on ESP32) - * - Stateful lambdas (with capture) → pointer to std::function (4 bytes on ESP32) - * Total size: enum (1 byte) + union (4 bytes) + padding = 8 bytes (same as PR #11555) - * - * Both lambda types must return optional (as YAML codegen does) to support the pattern: - * return {}; // Don't publish a value - * return 42.0; // Publish this value - * - * operator() returns optional, returning nullopt when no lambda is set (type == NONE). - * This eliminates redundant "is lambda set" checks by reusing optional's discriminator. - * - * @tparam T The return type (e.g., float for TemplateLambda>) - * @tparam Args Optional arguments for the lambda - */ -template class TemplateLambda { - public: - TemplateLambda() : type_(NONE) {} - - // For stateless lambdas: use function pointer - template - requires std::invocable && std::convertible_to < F, optional(*) - (Args...) > void set(F f) { - this->reset_(); - this->type_ = STATELESS_LAMBDA; - this->stateless_f_ = f; // Implicit conversion to function pointer - } - - // For stateful lambdas: use std::function pointer - template - requires std::invocable && - (!std::convertible_to (*)(Args...)>) &&std::convertible_to, - optional> void set(F &&f) { - this->reset_(); - this->type_ = LAMBDA; - this->f_ = new std::function(Args...)>(std::forward(f)); - } - - ~TemplateLambda() { this->reset_(); } - - // Copy constructor - TemplateLambda(const TemplateLambda &) = delete; - TemplateLambda &operator=(const TemplateLambda &) = delete; - - // Move constructor - TemplateLambda(TemplateLambda &&other) noexcept : type_(other.type_) { - if (type_ == LAMBDA) { - this->f_ = other.f_; - other.f_ = nullptr; - } else if (type_ == STATELESS_LAMBDA) { - this->stateless_f_ = other.stateless_f_; - } - other.type_ = NONE; - } - - TemplateLambda &operator=(TemplateLambda &&other) noexcept { - if (this != &other) { - this->reset_(); - this->type_ = other.type_; - if (type_ == LAMBDA) { - this->f_ = other.f_; - other.f_ = nullptr; - } else if (type_ == STATELESS_LAMBDA) { - this->stateless_f_ = other.stateless_f_; - } - other.type_ = NONE; - } - return *this; - } - - bool has_value() const { return this->type_ != NONE; } - - // Returns optional, returning nullopt if no lambda is set - optional operator()(Args... args) { - switch (this->type_) { - case STATELESS_LAMBDA: - return this->stateless_f_(args...); // Direct function pointer call - case LAMBDA: - return (*this->f_)(args...); // std::function call via pointer - case NONE: - default: - return nullopt; // No lambda set - } - } - - optional call(Args... args) { return (*this)(args...); } - - protected: - void reset_() { - if (this->type_ == LAMBDA) { - delete this->f_; - this->f_ = nullptr; - } - this->type_ = NONE; - } - - enum : uint8_t { - NONE, - STATELESS_LAMBDA, - LAMBDA, - } type_; - - union { - optional (*stateless_f_)(Args...); // Function pointer (4 bytes on ESP32) - std::function(Args...)> *f_; // Pointer to std::function (4 bytes on ESP32) - }; -}; - -} // namespace esphome diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index 7d2cb19077..da449111a2 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -10,26 +10,12 @@ esphome: state: !lambda "return 42.0;" # Test C++ API: set_template() with stateless lambda (no captures) + # IMPORTANT: set_template() is an internal API. Only stateless lambdas are supported. - lambda: |- id(template_sens).set_template([]() -> esphome::optional { return 123.0f; }); - # Test C++ API: set_template() with stateful lambda (with captures) - # This is the regression test for issue #11555 - - lambda: |- - float captured_value = 456.0f; - id(template_sens).set_template([captured_value]() -> esphome::optional { - return captured_value; - }); - - # Test C++ API: set_template() with more complex capture - - lambda: |- - auto sensor_id = id(template_sens); - id(template_number).set_template([sensor_id]() -> esphome::optional { - return sensor_id->state * 2.0f; - }); - - datetime.date.set: id: test_date date: From a849ddd57d288d984dcd91c316104608a86a4b00 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 16:10:32 -0500 Subject: [PATCH 224/394] wip --- .../binary_sensor/template_binary_sensor.h | 2 +- .../template/cover/template_cover.h | 2 +- .../template/datetime/template_date.h | 2 +- .../template/datetime/template_datetime.h | 2 +- .../template/datetime/template_time.h | 2 +- .../components/template/lock/template_lock.h | 2 +- .../template/number/template_number.h | 2 +- .../template/select/template_select.h | 2 +- .../template/sensor/template_sensor.h | 2 +- .../template/switch/template_switch.h | 2 +- esphome/components/template/template_lambda.h | 51 ------------------- .../components/template/text/template_text.h | 2 +- .../text_sensor/template_text_sensor.h | 2 +- .../template/valve/template_valve.h | 2 +- tests/components/template/common-base.yaml | 2 +- 15 files changed, 14 insertions(+), 65 deletions(-) delete mode 100644 esphome/components/template/template_lambda.h diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.h b/esphome/components/template/binary_sensor/template_binary_sensor.h index f63738d93f..0373f898a8 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.h +++ b/esphome/components/template/binary_sensor/template_binary_sensor.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/binary_sensor/binary_sensor.h" namespace esphome { diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index 57a5e11c5c..56ab61c3fb 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/cover/cover.h" namespace esphome { diff --git a/esphome/components/template/datetime/template_date.h b/esphome/components/template/datetime/template_date.h index b0fdbcbfbb..7fed704d0e 100644 --- a/esphome/components/template/datetime/template_date.h +++ b/esphome/components/template/datetime/template_date.h @@ -9,7 +9,7 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/time.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { diff --git a/esphome/components/template/datetime/template_datetime.h b/esphome/components/template/datetime/template_datetime.h index b1c94d7d34..ec45bf0326 100644 --- a/esphome/components/template/datetime/template_datetime.h +++ b/esphome/components/template/datetime/template_datetime.h @@ -9,7 +9,7 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/time.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { diff --git a/esphome/components/template/datetime/template_time.h b/esphome/components/template/datetime/template_time.h index c6938fe7a0..ea7474c0ba 100644 --- a/esphome/components/template/datetime/template_time.h +++ b/esphome/components/template/datetime/template_time.h @@ -9,7 +9,7 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/time.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { diff --git a/esphome/components/template/lock/template_lock.h b/esphome/components/template/lock/template_lock.h index 2bb11bfef6..347c4effb3 100644 --- a/esphome/components/template/lock/template_lock.h +++ b/esphome/components/template/lock/template_lock.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/lock/lock.h" namespace esphome { diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h index 1a6e9d964f..a9307e9246 100644 --- a/esphome/components/template/number/template_number.h +++ b/esphome/components/template/number/template_number.h @@ -4,7 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 53cadfa303..1c33153872 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -4,7 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { diff --git a/esphome/components/template/sensor/template_sensor.h b/esphome/components/template/sensor/template_sensor.h index 27980414f9..793d754a0f 100644 --- a/esphome/components/template/sensor/template_sensor.h +++ b/esphome/components/template/sensor/template_sensor.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/sensor/sensor.h" namespace esphome { diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index f436f657ae..47154fd047 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/switch/switch.h" namespace esphome { diff --git a/esphome/components/template/template_lambda.h b/esphome/components/template/template_lambda.h deleted file mode 100644 index 894b5edffa..0000000000 --- a/esphome/components/template/template_lambda.h +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once - -#include "esphome/core/optional.h" - -namespace esphome { - -/** Lightweight wrapper for template platform lambdas (stateless function pointers only). - * - * This optimizes template platforms by storing only a function pointer (4 bytes on ESP32) - * instead of std::function (16-32 bytes). - * - * IMPORTANT: This only supports stateless lambdas (no captures). The set_template() method - * is an internal API used by YAML codegen, not intended for external use. - * - * Lambdas must return optional to support the pattern: - * return {}; // Don't publish a value - * return 42.0; // Publish this value - * - * operator() returns optional, returning nullopt when no lambda is set (nullptr check). - * - * @tparam T The return type (e.g., float for sensor values) - * @tparam Args Optional arguments for the lambda - */ -template class TemplateLambda { - public: - TemplateLambda() : f_(nullptr) {} - - /** Set the lambda function pointer. - * INTERNAL API: Only for use by YAML codegen. - * Only stateless lambdas (no captures) are supported. - */ - void set(optional (*f)(Args...)) { this->f_ = f; } - - /** Check if a lambda is set */ - bool has_value() const { return this->f_ != nullptr; } - - /** Call the lambda, returning nullopt if no lambda is set */ - optional operator()(Args... args) { - if (this->f_ == nullptr) - return nullopt; - return this->f_(args...); - } - - /** Alias for operator() for compatibility */ - optional call(Args... args) { return (*this)(args...); } - - protected: - optional (*f_)(Args...); // Function pointer (4 bytes on ESP32) -}; - -} // namespace esphome diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index fd28800bab..c12021f80e 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -4,7 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { diff --git a/esphome/components/template/text_sensor/template_text_sensor.h b/esphome/components/template/text_sensor/template_text_sensor.h index fda28f53c7..0d01c72023 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.h +++ b/esphome/components/template/text_sensor/template_text_sensor.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/text_sensor/text_sensor.h" namespace esphome { diff --git a/esphome/components/template/valve/template_valve.h b/esphome/components/template/valve/template_valve.h index 7713091673..23a77ff918 100644 --- a/esphome/components/template/valve/template_valve.h +++ b/esphome/components/template/valve/template_valve.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/valve/valve.h" namespace esphome { diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index da449111a2..f101eea942 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -10,7 +10,7 @@ esphome: state: !lambda "return 42.0;" # Test C++ API: set_template() with stateless lambda (no captures) - # IMPORTANT: set_template() is an internal API. Only stateless lambdas are supported. + # NOTE: set_template() is not intended to be a public API, but we test it to ensure it doesn't break. - lambda: |- id(template_sens).set_template([]() -> esphome::optional { return 123.0f; From 922acda1a808235c8c252c156d4374a53c3e2e7c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 16:12:05 -0500 Subject: [PATCH 225/394] wip --- esphome/core/template_lambda.h | 51 ++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 esphome/core/template_lambda.h diff --git a/esphome/core/template_lambda.h b/esphome/core/template_lambda.h new file mode 100644 index 0000000000..894b5edffa --- /dev/null +++ b/esphome/core/template_lambda.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/core/optional.h" + +namespace esphome { + +/** Lightweight wrapper for template platform lambdas (stateless function pointers only). + * + * This optimizes template platforms by storing only a function pointer (4 bytes on ESP32) + * instead of std::function (16-32 bytes). + * + * IMPORTANT: This only supports stateless lambdas (no captures). The set_template() method + * is an internal API used by YAML codegen, not intended for external use. + * + * Lambdas must return optional to support the pattern: + * return {}; // Don't publish a value + * return 42.0; // Publish this value + * + * operator() returns optional, returning nullopt when no lambda is set (nullptr check). + * + * @tparam T The return type (e.g., float for sensor values) + * @tparam Args Optional arguments for the lambda + */ +template class TemplateLambda { + public: + TemplateLambda() : f_(nullptr) {} + + /** Set the lambda function pointer. + * INTERNAL API: Only for use by YAML codegen. + * Only stateless lambdas (no captures) are supported. + */ + void set(optional (*f)(Args...)) { this->f_ = f; } + + /** Check if a lambda is set */ + bool has_value() const { return this->f_ != nullptr; } + + /** Call the lambda, returning nullopt if no lambda is set */ + optional operator()(Args... args) { + if (this->f_ == nullptr) + return nullopt; + return this->f_(args...); + } + + /** Alias for operator() for compatibility */ + optional call(Args... args) { return (*this)(args...); } + + protected: + optional (*f_)(Args...); // Function pointer (4 bytes on ESP32) +}; + +} // namespace esphome From 68d1a7e3effc93411549924234cedb6e6ef64ef8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 16:15:15 -0500 Subject: [PATCH 226/394] wip --- .../components/template/binary_sensor/template_binary_sensor.h | 2 +- esphome/components/template/cover/template_cover.h | 2 +- esphome/components/template/datetime/template_date.h | 2 +- esphome/components/template/datetime/template_datetime.h | 2 +- esphome/components/template/datetime/template_time.h | 2 +- esphome/components/template/lock/template_lock.h | 2 +- esphome/components/template/number/template_number.h | 2 +- esphome/components/template/select/template_select.h | 2 +- esphome/components/template/sensor/template_sensor.h | 2 +- esphome/components/template/switch/template_switch.h | 2 +- esphome/{core => components/template}/template_lambda.h | 0 esphome/components/template/text/template_text.h | 2 +- esphome/components/template/text_sensor/template_text_sensor.h | 2 +- esphome/components/template/valve/template_valve.h | 2 +- 14 files changed, 13 insertions(+), 13 deletions(-) rename esphome/{core => components/template}/template_lambda.h (100%) diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.h b/esphome/components/template/binary_sensor/template_binary_sensor.h index 0373f898a8..f63738d93f 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.h +++ b/esphome/components/template/binary_sensor/template_binary_sensor.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" #include "esphome/components/binary_sensor/binary_sensor.h" namespace esphome { diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index 56ab61c3fb..57a5e11c5c 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" #include "esphome/components/cover/cover.h" namespace esphome { diff --git a/esphome/components/template/datetime/template_date.h b/esphome/components/template/datetime/template_date.h index 7fed704d0e..b0fdbcbfbb 100644 --- a/esphome/components/template/datetime/template_date.h +++ b/esphome/components/template/datetime/template_date.h @@ -9,7 +9,7 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/time.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" namespace esphome { namespace template_ { diff --git a/esphome/components/template/datetime/template_datetime.h b/esphome/components/template/datetime/template_datetime.h index ec45bf0326..b1c94d7d34 100644 --- a/esphome/components/template/datetime/template_datetime.h +++ b/esphome/components/template/datetime/template_datetime.h @@ -9,7 +9,7 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/time.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" namespace esphome { namespace template_ { diff --git a/esphome/components/template/datetime/template_time.h b/esphome/components/template/datetime/template_time.h index ea7474c0ba..c6938fe7a0 100644 --- a/esphome/components/template/datetime/template_time.h +++ b/esphome/components/template/datetime/template_time.h @@ -9,7 +9,7 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/time.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" namespace esphome { namespace template_ { diff --git a/esphome/components/template/lock/template_lock.h b/esphome/components/template/lock/template_lock.h index 347c4effb3..2bb11bfef6 100644 --- a/esphome/components/template/lock/template_lock.h +++ b/esphome/components/template/lock/template_lock.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" #include "esphome/components/lock/lock.h" namespace esphome { diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h index a9307e9246..1a6e9d964f 100644 --- a/esphome/components/template/number/template_number.h +++ b/esphome/components/template/number/template_number.h @@ -4,7 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" namespace esphome { namespace template_ { diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 1c33153872..53cadfa303 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -4,7 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" namespace esphome { namespace template_ { diff --git a/esphome/components/template/sensor/template_sensor.h b/esphome/components/template/sensor/template_sensor.h index 793d754a0f..27980414f9 100644 --- a/esphome/components/template/sensor/template_sensor.h +++ b/esphome/components/template/sensor/template_sensor.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" #include "esphome/components/sensor/sensor.h" namespace esphome { diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index 47154fd047..f436f657ae 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" #include "esphome/components/switch/switch.h" namespace esphome { diff --git a/esphome/core/template_lambda.h b/esphome/components/template/template_lambda.h similarity index 100% rename from esphome/core/template_lambda.h rename to esphome/components/template/template_lambda.h diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index c12021f80e..fd28800bab 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -4,7 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" namespace esphome { namespace template_ { diff --git a/esphome/components/template/text_sensor/template_text_sensor.h b/esphome/components/template/text_sensor/template_text_sensor.h index 0d01c72023..fda28f53c7 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.h +++ b/esphome/components/template/text_sensor/template_text_sensor.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" #include "esphome/components/text_sensor/text_sensor.h" namespace esphome { diff --git a/esphome/components/template/valve/template_valve.h b/esphome/components/template/valve/template_valve.h index 23a77ff918..7713091673 100644 --- a/esphome/components/template/valve/template_valve.h +++ b/esphome/components/template/valve/template_valve.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" -#include "esphome/core/template_lambda.h" +#include "../template_lambda.h" #include "esphome/components/valve/valve.h" namespace esphome { From 5478fa69e9c550c18a54bdfa3f83e7a9b0ef2c10 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 16:20:11 -0500 Subject: [PATCH 227/394] twip --- .../components/template/binary_sensor/template_binary_sensor.h | 2 +- esphome/components/template/cover/template_cover.h | 2 +- esphome/components/template/datetime/template_date.h | 2 +- esphome/components/template/datetime/template_datetime.h | 2 +- esphome/components/template/datetime/template_time.h | 2 +- esphome/components/template/lock/template_lock.h | 2 +- esphome/components/template/number/template_number.h | 2 +- esphome/components/template/select/template_select.h | 2 +- esphome/components/template/sensor/template_sensor.h | 2 +- esphome/components/template/switch/template_switch.h | 2 +- esphome/components/template/text/template_text.h | 2 +- esphome/components/template/text_sensor/template_text_sensor.h | 2 +- esphome/components/template/valve/template_valve.h | 2 +- esphome/{components/template => core}/template_lambda.h | 0 14 files changed, 13 insertions(+), 13 deletions(-) rename esphome/{components/template => core}/template_lambda.h (100%) diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.h b/esphome/components/template/binary_sensor/template_binary_sensor.h index f63738d93f..0373f898a8 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.h +++ b/esphome/components/template/binary_sensor/template_binary_sensor.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/binary_sensor/binary_sensor.h" namespace esphome { diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index 57a5e11c5c..56ab61c3fb 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/cover/cover.h" namespace esphome { diff --git a/esphome/components/template/datetime/template_date.h b/esphome/components/template/datetime/template_date.h index b0fdbcbfbb..7fed704d0e 100644 --- a/esphome/components/template/datetime/template_date.h +++ b/esphome/components/template/datetime/template_date.h @@ -9,7 +9,7 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/time.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { diff --git a/esphome/components/template/datetime/template_datetime.h b/esphome/components/template/datetime/template_datetime.h index b1c94d7d34..ec45bf0326 100644 --- a/esphome/components/template/datetime/template_datetime.h +++ b/esphome/components/template/datetime/template_datetime.h @@ -9,7 +9,7 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/time.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { diff --git a/esphome/components/template/datetime/template_time.h b/esphome/components/template/datetime/template_time.h index c6938fe7a0..ea7474c0ba 100644 --- a/esphome/components/template/datetime/template_time.h +++ b/esphome/components/template/datetime/template_time.h @@ -9,7 +9,7 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/time.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { diff --git a/esphome/components/template/lock/template_lock.h b/esphome/components/template/lock/template_lock.h index 2bb11bfef6..347c4effb3 100644 --- a/esphome/components/template/lock/template_lock.h +++ b/esphome/components/template/lock/template_lock.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/lock/lock.h" namespace esphome { diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h index 1a6e9d964f..a9307e9246 100644 --- a/esphome/components/template/number/template_number.h +++ b/esphome/components/template/number/template_number.h @@ -4,7 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 53cadfa303..1c33153872 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -4,7 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { diff --git a/esphome/components/template/sensor/template_sensor.h b/esphome/components/template/sensor/template_sensor.h index 27980414f9..793d754a0f 100644 --- a/esphome/components/template/sensor/template_sensor.h +++ b/esphome/components/template/sensor/template_sensor.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/sensor/sensor.h" namespace esphome { diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index f436f657ae..47154fd047 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/switch/switch.h" namespace esphome { diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index fd28800bab..c12021f80e 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -4,7 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { diff --git a/esphome/components/template/text_sensor/template_text_sensor.h b/esphome/components/template/text_sensor/template_text_sensor.h index fda28f53c7..0d01c72023 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.h +++ b/esphome/components/template/text_sensor/template_text_sensor.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/text_sensor/text_sensor.h" namespace esphome { diff --git a/esphome/components/template/valve/template_valve.h b/esphome/components/template/valve/template_valve.h index 7713091673..23a77ff918 100644 --- a/esphome/components/template/valve/template_valve.h +++ b/esphome/components/template/valve/template_valve.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" -#include "../template_lambda.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/valve/valve.h" namespace esphome { diff --git a/esphome/components/template/template_lambda.h b/esphome/core/template_lambda.h similarity index 100% rename from esphome/components/template/template_lambda.h rename to esphome/core/template_lambda.h From fe1270e4c1ae0169406ea8d083f38164ae67d383 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 16:45:29 -0500 Subject: [PATCH 228/394] forward args --- esphome/core/template_lambda.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/core/template_lambda.h b/esphome/core/template_lambda.h index 894b5edffa..7b8f4374aa 100644 --- a/esphome/core/template_lambda.h +++ b/esphome/core/template_lambda.h @@ -35,14 +35,14 @@ template class TemplateLambda { bool has_value() const { return this->f_ != nullptr; } /** Call the lambda, returning nullopt if no lambda is set */ - optional operator()(Args... args) { + optional operator()(Args &&...args) { if (this->f_ == nullptr) return nullopt; - return this->f_(args...); + return this->f_(std::forward(args)...); } /** Alias for operator() for compatibility */ - optional call(Args... args) { return (*this)(args...); } + optional call(Args &&...args) { return (*this)(std::forward(args)...); } protected: optional (*f_)(Args...); // Function pointer (4 bytes on ESP32) From d2f1baa800a9a503c3e2498be7131670dcf243d2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 16:53:13 -0500 Subject: [PATCH 229/394] remove enable_loops, not needed since setup runs after setters, since setters are called in main setup() before component setup() --- .../template/binary_sensor/template_binary_sensor.h | 5 +---- esphome/components/template/cover/template_cover.h | 10 ++-------- esphome/components/template/lock/template_lock.h | 5 +---- esphome/components/template/switch/template_switch.h | 5 +---- esphome/components/template/valve/template_valve.h | 5 +---- 5 files changed, 6 insertions(+), 24 deletions(-) diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.h b/esphome/components/template/binary_sensor/template_binary_sensor.h index 0373f898a8..bc591391b9 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.h +++ b/esphome/components/template/binary_sensor/template_binary_sensor.h @@ -9,10 +9,7 @@ namespace template_ { class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor { public: - template void set_template(F &&f) { - this->f_.set(std::forward(f)); - this->enable_loop(); - } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void loop() override; diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index 56ab61c3fb..faff69f867 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -18,14 +18,8 @@ class TemplateCover : public cover::Cover, public Component { public: TemplateCover(); - template void set_state_lambda(F &&f) { - this->state_f_.set(std::forward(f)); - this->enable_loop(); - } - template void set_tilt_lambda(F &&f) { - this->tilt_f_.set(std::forward(f)); - this->enable_loop(); - } + template void set_state_lambda(F &&f) { this->state_f_.set(std::forward(f)); } + template void set_tilt_lambda(F &&f) { this->tilt_f_.set(std::forward(f)); } Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; diff --git a/esphome/components/template/lock/template_lock.h b/esphome/components/template/lock/template_lock.h index 347c4effb3..de5189875f 100644 --- a/esphome/components/template/lock/template_lock.h +++ b/esphome/components/template/lock/template_lock.h @@ -14,10 +14,7 @@ class TemplateLock : public lock::Lock, public Component { void dump_config() override; - template void set_state_lambda(F &&f) { - this->f_.set(std::forward(f)); - this->enable_loop(); - } + template void set_state_lambda(F &&f) { this->f_.set(std::forward(f)); } Trigger<> *get_lock_trigger() const; Trigger<> *get_unlock_trigger() const; Trigger<> *get_open_trigger() const; diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index 47154fd047..18a374df35 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -15,10 +15,7 @@ class TemplateSwitch : public switch_::Switch, public Component { void setup() override; void dump_config() override; - template void set_state_lambda(F &&f) { - this->f_.set(std::forward(f)); - this->enable_loop(); - } + template void set_state_lambda(F &&f) { this->f_.set(std::forward(f)); } Trigger<> *get_turn_on_trigger() const; Trigger<> *get_turn_off_trigger() const; void set_optimistic(bool optimistic); diff --git a/esphome/components/template/valve/template_valve.h b/esphome/components/template/valve/template_valve.h index 23a77ff918..d6235f8e5c 100644 --- a/esphome/components/template/valve/template_valve.h +++ b/esphome/components/template/valve/template_valve.h @@ -18,10 +18,7 @@ class TemplateValve : public valve::Valve, public Component { public: TemplateValve(); - template void set_state_lambda(F &&f) { - this->state_f_.set(std::forward(f)); - this->enable_loop(); - } + template void set_state_lambda(F &&f) { this->state_f_.set(std::forward(f)); } Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; From ec128914a3ac791438f2406ee3292dbc295253ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 16:55:41 -0500 Subject: [PATCH 230/394] missing disable in lock --- esphome/components/template/lock/template_lock.cpp | 5 +++++ esphome/components/template/lock/template_lock.h | 1 + 2 files changed, 6 insertions(+) diff --git a/esphome/components/template/lock/template_lock.cpp b/esphome/components/template/lock/template_lock.cpp index 634924a805..8ed87b9736 100644 --- a/esphome/components/template/lock/template_lock.cpp +++ b/esphome/components/template/lock/template_lock.cpp @@ -11,6 +11,11 @@ static const char *const TAG = "template.lock"; TemplateLock::TemplateLock() : lock_trigger_(new Trigger<>()), unlock_trigger_(new Trigger<>()), open_trigger_(new Trigger<>()) {} +void TemplateLock::setup() { + if (!this->f_.has_value()) + this->disable_loop(); +} + void TemplateLock::loop() { auto val = this->f_(); if (val.has_value()) { diff --git a/esphome/components/template/lock/template_lock.h b/esphome/components/template/lock/template_lock.h index de5189875f..14fca4635e 100644 --- a/esphome/components/template/lock/template_lock.h +++ b/esphome/components/template/lock/template_lock.h @@ -12,6 +12,7 @@ class TemplateLock : public lock::Lock, public Component { public: TemplateLock(); + void setup() override; void dump_config() override; template void set_state_lambda(F &&f) { this->f_.set(std::forward(f)); } From af6581bfed34b678abc20754547f42663e841e19 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 16:55:52 -0500 Subject: [PATCH 231/394] missing disable in lock --- .../template/lock/template_lock.h.bak | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 esphome/components/template/lock/template_lock.h.bak diff --git a/esphome/components/template/lock/template_lock.h.bak b/esphome/components/template/lock/template_lock.h.bak new file mode 100644 index 0000000000..4f798eca81 --- /dev/null +++ b/esphome/components/template/lock/template_lock.h.bak @@ -0,0 +1,38 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/lock/lock.h" + +namespace esphome { +namespace template_ { + +class TemplateLock : public lock::Lock, public Component { + public: + TemplateLock(); + + void dump_config() override; + + void set_state_lambda(std::function()> &&f); + Trigger<> *get_lock_trigger() const; + Trigger<> *get_unlock_trigger() const; + Trigger<> *get_open_trigger() const; + void set_optimistic(bool optimistic); + void loop() override; + + float get_setup_priority() const override; + + protected: + void control(const lock::LockCall &call) override; + void open_latch() override; + + optional()>> f_; + bool optimistic_{false}; + Trigger<> *lock_trigger_; + Trigger<> *unlock_trigger_; + Trigger<> *open_trigger_; + Trigger<> *prev_trigger_{nullptr}; +}; + +} // namespace template_ +} // namespace esphome From 22b718a87d552acb20966f66cd9094056e186b12 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 16:56:01 -0500 Subject: [PATCH 232/394] missing disable in lock --- .../template/lock/template_lock.h.bak | 38 ------------------- 1 file changed, 38 deletions(-) delete mode 100644 esphome/components/template/lock/template_lock.h.bak diff --git a/esphome/components/template/lock/template_lock.h.bak b/esphome/components/template/lock/template_lock.h.bak deleted file mode 100644 index 4f798eca81..0000000000 --- a/esphome/components/template/lock/template_lock.h.bak +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/core/automation.h" -#include "esphome/components/lock/lock.h" - -namespace esphome { -namespace template_ { - -class TemplateLock : public lock::Lock, public Component { - public: - TemplateLock(); - - void dump_config() override; - - void set_state_lambda(std::function()> &&f); - Trigger<> *get_lock_trigger() const; - Trigger<> *get_unlock_trigger() const; - Trigger<> *get_open_trigger() const; - void set_optimistic(bool optimistic); - void loop() override; - - float get_setup_priority() const override; - - protected: - void control(const lock::LockCall &call) override; - void open_latch() override; - - optional()>> f_; - bool optimistic_{false}; - Trigger<> *lock_trigger_; - Trigger<> *unlock_trigger_; - Trigger<> *open_trigger_; - Trigger<> *prev_trigger_{nullptr}; -}; - -} // namespace template_ -} // namespace esphome From b74378690881ebe5c5686c99c832e7923aeea611 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 17:45:18 -0500 Subject: [PATCH 233/394] merge --- tests/integration/fixtures/runtime_stats.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/fixtures/runtime_stats.yaml b/tests/integration/fixtures/runtime_stats.yaml index aad1c275fb..fd34cdb939 100644 --- a/tests/integration/fixtures/runtime_stats.yaml +++ b/tests/integration/fixtures/runtime_stats.yaml @@ -32,6 +32,7 @@ switch: name: "Test Switch" id: test_switch optimistic: true + lambda: return false; interval: - interval: 0.5s From 0ea74c2663a6e5c42490022f103459e83711e600 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 18:05:01 -0500 Subject: [PATCH 234/394] [gpio] Skip set_use_interrupt call when using default value --- esphome/components/gpio/binary_sensor/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/gpio/binary_sensor/__init__.py b/esphome/components/gpio/binary_sensor/__init__.py index 8372bc7e08..ca4dc43e9c 100644 --- a/esphome/components/gpio/binary_sensor/__init__.py +++ b/esphome/components/gpio/binary_sensor/__init__.py @@ -94,6 +94,8 @@ async def to_code(config): ) use_interrupt = False - cg.add(var.set_use_interrupt(use_interrupt)) if use_interrupt: cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) + else: + # Only generate call when disabling interrupts (default is true) + cg.add(var.set_use_interrupt(use_interrupt)) From 34f7ff42aebd3f66c29269855b240f77266f6473 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 18:13:16 -0500 Subject: [PATCH 235/394] merge --- esphome/components/usb_uart/usb_uart.cpp | 106 +---------------------- esphome/components/usb_uart/usb_uart.h | 8 +- 2 files changed, 2 insertions(+), 112 deletions(-) diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index dc9ba4af7a..c24fffb11d 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -169,98 +169,6 @@ bool USBUartChannel::read_array(uint8_t *data, size_t len) { this->parent_->start_input(this); return status; } -void USBUartComponent::reset_input_state_(USBUartChannel *channel) { - channel->input_retry_count_.store(0); - channel->input_started_.store(false); -} - -void USBUartComponent::restart_input_(USBUartChannel *channel) { - // Atomically verify it's still started (true) and keep it started - // This prevents the race window of toggling true->false->true - bool expected = true; - if (channel->input_started_.compare_exchange_strong(expected, true)) { - // Still started - do the actual restart work without toggling the flag - this->do_start_input_(channel); - } -} - -void USBUartComponent::input_transfer_callback_(USBUartChannel *channel, const usb_host::TransferStatus &status) { - // CALLBACK CONTEXT: This function is executed in USB task via transfer_callback - ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code); - - if (!status.success) { - ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code)); - // Transfer failed, slot already released - // Reset state so normal operations can restart later - this->reset_input_state_(channel); - return; - } - - if (!channel->dummy_receiver_ && status.data_len > 0) { - // Allocate a chunk from the pool - UsbDataChunk *chunk = this->chunk_pool_.allocate(); - if (chunk == nullptr) { - // No chunks available - queue is full, data dropped, slot already released - this->usb_data_queue_.increment_dropped_count(); - // Reset state so normal operations can restart later - this->reset_input_state_(channel); - return; - } - - // Copy data to chunk (this is fast, happens in USB task) - memcpy(chunk->data, status.data, status.data_len); - chunk->length = status.data_len; - chunk->channel = channel; - - // Push to lock-free queue for main loop processing - // Push always succeeds because pool size == queue size - this->usb_data_queue_.push(chunk); - } - - // On success, reset retry count and restart input immediately from USB task for performance - // The lock-free queue will handle backpressure - channel->input_retry_count_.store(0); - channel->input_started_.store(false); - this->start_input(channel); -} - -void USBUartComponent::do_start_input_(USBUartChannel *channel) { - // This function does the actual work of starting input - // Caller must ensure input_started_ is already set to true - const auto *ep = channel->cdc_dev_.in_ep; - - // input_started_ already set to true by caller - auto result = this->transfer_in( - ep->bEndpointAddress, - [this, channel](const usb_host::TransferStatus &status) { this->input_transfer_callback_(channel, status); }, - ep->wMaxPacketSize); - - if (result == usb_host::TRANSFER_ERROR_NO_SLOTS) { - // No slots available - defer retry to main loop - this->defer_input_retry_(channel); - } else if (result != usb_host::TRANSFER_OK) { - // Other error (submit failed) - don't retry, just reset state - // Error already logged by transfer_in() - this->reset_input_state_(channel); - } -} - -void USBUartComponent::defer_input_retry_(USBUartChannel *channel) { - static constexpr uint8_t MAX_INPUT_RETRIES = 10; - - // Atomically increment and get the NEW value (previous + 1) - uint8_t new_retry_count = channel->input_retry_count_.fetch_add(1) + 1; - if (new_retry_count > MAX_INPUT_RETRIES) { - ESP_LOGE(TAG, "Input retry limit reached for channel %d, stopping retries", channel->index_); - this->reset_input_state_(channel); - return; - } - - // Keep input_started_ as true during defer to prevent multiple retries from queueing - // The deferred lambda will atomically restart - this->defer([this, channel] { this->restart_input_(channel); }); -} - void USBUartComponent::setup() { USBClient::setup(); } void USBUartComponent::loop() { USBClient::loop(); @@ -308,12 +216,6 @@ void USBUartComponent::dump_config() { void USBUartComponent::start_input(USBUartChannel *channel) { if (!channel->initialised_.load()) return; - - // Atomically check if not started and set to started in one operation - bool expected = false; - if (!channel->input_started_.compare_exchange_strong(expected, true)) - return; // Already started - prevents duplicate transfers from concurrent threads - // THREAD CONTEXT: Called from both USB task and main loop threads // - USB task: Immediate restart after successful transfer for continuous data flow // - Main loop: Controlled restart after consuming data (backpressure mechanism) @@ -324,11 +226,6 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // // The underlying transfer_in() uses lock-free atomic allocation from the // TransferRequest pool, making this multi-threaded access safe -<<<<<<< HEAD - - // Do the actual work (input_started_ already set to true by CAS above) - this->do_start_input_(channel); -======= // if already started, don't restart. A spurious failure in compare_exchange_weak // is not a problem, as it will be retried on the next read_array() @@ -375,7 +272,6 @@ void USBUartComponent::start_input(USBUartChannel *channel) { if (!this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize)) { channel->input_started_.store(false); } ->>>>>>> clydebarrow/usb-uart } void USBUartComponent::start_output(USBUartChannel *channel) { @@ -482,7 +378,7 @@ void USBUartTypeCdcAcm::enable_channels() { for (auto *channel : this->channels_) { if (!channel->initialised_.load()) continue; - this->reset_input_state_(channel); + channel->input_started_.store(false); channel->output_started_.store(false); this->start_input(channel); } diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index ba7fc3ebe5..a5e7905ac5 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -111,11 +111,10 @@ class USBUartChannel : public uart::UARTComponent, public Parented input_started_{true}; std::atomic output_started_{true}; std::atomic initialised_{false}; - std::atomic input_retry_count_{0}; // Group regular bytes together to minimize padding const uint8_t index_; bool debug_{}; @@ -141,11 +140,6 @@ class USBUartComponent : public usb_host::USBClient { EventPool chunk_pool_; protected: - void defer_input_retry_(USBUartChannel *channel); - void reset_input_state_(USBUartChannel *channel); - void restart_input_(USBUartChannel *channel); - void do_start_input_(USBUartChannel *channel); - void input_transfer_callback_(USBUartChannel *channel, const usb_host::TransferStatus &status); std::vector channels_{}; }; From 03fd1143710d58243a93f5901dc49e677413b981 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 20:26:37 -0500 Subject: [PATCH 236/394] [ci] Restore parallel execution for clang-tidy split mode (#11613) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd45adb78b..a2b1f84ca8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -458,7 +458,7 @@ jobs: GH_TOKEN: ${{ github.token }} strategy: fail-fast: false - max-parallel: 1 + max-parallel: 2 matrix: include: - id: clang-tidy From 08aae39ea47d65dd4ff4af655fd967f2a64167f7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 20:27:28 -0500 Subject: [PATCH 237/394] [ci] Consolidate component splitting into determine-jobs (#11614) --- .github/workflows/ci.yml | 46 ++--------------------------- script/determine-jobs.py | 24 +++++++++++++++ script/split_components_for_ci.py | 4 +++ tests/script/test_determine_jobs.py | 11 +++++++ 4 files changed, 42 insertions(+), 43 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a2b1f84ca8..1756d5b765 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -180,6 +180,7 @@ jobs: memory_impact: ${{ steps.determine.outputs.memory-impact }} cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }} cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }} + component-test-batches: ${{ steps.determine.outputs.component-test-batches }} steps: - name: Check out code from GitHub uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -214,6 +215,7 @@ jobs: echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT + echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT integration-tests: name: Run integration tests @@ -536,59 +538,18 @@ jobs: run: script/ci-suggest-changes if: always() - test-build-components-splitter: - name: Split components for intelligent grouping (40 weighted per batch) - runs-on: ubuntu-24.04 - needs: - - common - - determine-jobs - if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 - outputs: - matrix: ${{ steps.split.outputs.components }} - steps: - - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Restore Python - uses: ./.github/actions/restore-python - with: - python-version: ${{ env.DEFAULT_PYTHON }} - cache-key: ${{ needs.common.outputs.cache-key }} - - name: Split components intelligently based on bus configurations - id: split - run: | - . venv/bin/activate - - # Use intelligent splitter that groups components with same bus configs - components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}' - - # Only isolate directly changed components when targeting dev branch - # For beta/release branches, group everything for faster CI - if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then - directly_changed='[]' - echo "Target branch: ${{ github.base_ref }} - grouping all components" - else - directly_changed='${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}' - echo "Target branch: ${{ github.base_ref }} - isolating directly changed components" - fi - - echo "Splitting components intelligently..." - output=$(python3 script/split_components_for_ci.py --components "$components" --directly-changed "$directly_changed" --batch-size 40 --output github) - - echo "$output" >> $GITHUB_OUTPUT - test-build-components-split: name: Test components batch (${{ matrix.components }}) runs-on: ubuntu-24.04 needs: - common - determine-jobs - - test-build-components-splitter if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 strategy: fail-fast: false max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }} matrix: - components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }} + components: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }} steps: - name: Show disk space run: | @@ -980,7 +941,6 @@ jobs: - clang-tidy-nosplit - clang-tidy-split - determine-jobs - - test-build-components-splitter - test-build-components-split - pre-commit-ci-lite - memory-impact-target-branch diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 21eb529f33..4a0edebb0d 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -43,12 +43,14 @@ from enum import StrEnum from functools import cache import json import os +from pathlib import Path import subprocess import sys from typing import Any from helpers import ( CPP_FILE_EXTENSIONS, + ESPHOME_TESTS_COMPONENTS_PATH, PYTHON_FILE_EXTENSIONS, changed_files, core_changed, @@ -65,12 +67,17 @@ from helpers import ( parse_test_filename, root_path, ) +from split_components_for_ci import create_intelligent_batches # Threshold for splitting clang-tidy jobs # For small PRs (< 65 files), use nosplit for faster CI # For large PRs (>= 65 files), use split for better parallelization CLANG_TIDY_SPLIT_THRESHOLD = 65 +# Component test batch size (weighted) +# Isolated components count as 10x, groupable components count as 1x +COMPONENT_TEST_BATCH_SIZE = 40 + class Platform(StrEnum): """Platform identifiers for memory impact analysis.""" @@ -686,6 +693,22 @@ def main() -> None: # Determine which C++ unit tests to run cpp_run_all, cpp_components = determine_cpp_unit_tests(args.branch) + # Split components into batches for CI testing + # This intelligently groups components with similar bus configurations + component_test_batches: list[str] + if changed_components_with_tests: + tests_dir = Path(root_path) / ESPHOME_TESTS_COMPONENTS_PATH + batches, _ = create_intelligent_batches( + components=changed_components_with_tests, + tests_dir=tests_dir, + batch_size=COMPONENT_TEST_BATCH_SIZE, + directly_changed=directly_changed_with_tests, + ) + # Convert batches to space-separated strings for CI matrix + component_test_batches = [" ".join(batch) for batch in batches] + else: + component_test_batches = [] + output: dict[str, Any] = { "integration_tests": run_integration, "clang_tidy": run_clang_tidy, @@ -703,6 +726,7 @@ def main() -> None: "memory_impact": memory_impact, "cpp_unit_tests_run_all": cpp_run_all, "cpp_unit_tests_components": cpp_components, + "component_test_batches": component_test_batches, } # Output as JSON diff --git a/script/split_components_for_ci.py b/script/split_components_for_ci.py index 87da540d43..65d09efb9b 100755 --- a/script/split_components_for_ci.py +++ b/script/split_components_for_ci.py @@ -62,6 +62,10 @@ def create_intelligent_batches( ) -> tuple[list[list[str]], dict[tuple[str, str], list[str]]]: """Create batches optimized for component grouping. + IMPORTANT: This function is called from both split_components_for_ci.py (standalone script) + and determine-jobs.py (integrated into job determination). Be careful when refactoring + to ensure changes work in both contexts. + Args: components: List of component names to batch tests_dir: Path to tests/components directory diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index c8ef76184f..e73c134151 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -152,6 +152,14 @@ def test_main_all_tests_should_run( assert output["memory_impact"]["should_run"] == "false" assert output["cpp_unit_tests_run_all"] is False assert output["cpp_unit_tests_components"] == ["wifi", "api", "sensor"] + # component_test_batches should be present and be a list of space-separated strings + assert "component_test_batches" in output + assert isinstance(output["component_test_batches"], list) + # Each batch should be a space-separated string of component names + for batch in output["component_test_batches"]: + assert isinstance(batch, str) + # Should contain at least one component (no empty batches) + assert len(batch) > 0 def test_main_no_tests_should_run( @@ -209,6 +217,9 @@ def test_main_no_tests_should_run( assert output["memory_impact"]["should_run"] == "false" assert output["cpp_unit_tests_run_all"] is False assert output["cpp_unit_tests_components"] == [] + # component_test_batches should be empty list + assert "component_test_batches" in output + assert output["component_test_batches"] == [] def test_main_with_branch_argument( From 29ed3c20af179fcdb2fd641566f7167fe79c863a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 20:28:38 -0500 Subject: [PATCH 238/394] [gpio] Skip set_use_interrupt call when using default value (#11612) --- esphome/components/gpio/binary_sensor/__init__.py | 4 +++- tests/component_tests/gpio/test_gpio_binary_sensor.py | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/esphome/components/gpio/binary_sensor/__init__.py b/esphome/components/gpio/binary_sensor/__init__.py index 8372bc7e08..ca4dc43e9c 100644 --- a/esphome/components/gpio/binary_sensor/__init__.py +++ b/esphome/components/gpio/binary_sensor/__init__.py @@ -94,6 +94,8 @@ async def to_code(config): ) use_interrupt = False - cg.add(var.set_use_interrupt(use_interrupt)) if use_interrupt: cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) + else: + # Only generate call when disabling interrupts (default is true) + cg.add(var.set_use_interrupt(use_interrupt)) diff --git a/tests/component_tests/gpio/test_gpio_binary_sensor.py b/tests/component_tests/gpio/test_gpio_binary_sensor.py index 74fa2ab1c1..73665dc45d 100644 --- a/tests/component_tests/gpio/test_gpio_binary_sensor.py +++ b/tests/component_tests/gpio/test_gpio_binary_sensor.py @@ -18,7 +18,8 @@ def test_gpio_binary_sensor_basic_setup( assert "new gpio::GPIOBinarySensor();" in main_cpp assert "App.register_binary_sensor" in main_cpp - assert "bs_gpio->set_use_interrupt(true);" in main_cpp + # set_use_interrupt(true) should NOT be generated (uses C++ default) + assert "bs_gpio->set_use_interrupt(true);" not in main_cpp assert "bs_gpio->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp @@ -51,8 +52,8 @@ def test_gpio_binary_sensor_esp8266_other_pins_use_interrupt( "tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml" ) - # GPIO5 should still use interrupts - assert "bs_gpio5->set_use_interrupt(true);" in main_cpp + # GPIO5 should still use interrupts (default, so no setter call) + assert "bs_gpio5->set_use_interrupt(true);" not in main_cpp assert "bs_gpio5->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp From 58235049e3c7a023a37441aed6b8440abaec0626 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 21:06:21 -0500 Subject: [PATCH 239/394] [template] Eliminate optional wrapper to save 4 bytes RAM per instance (#11610) --- .../binary_sensor/template_binary_sensor.cpp | 14 +++-- .../binary_sensor/template_binary_sensor.h | 5 +- .../template/cover/template_cover.cpp | 33 ++++++------ .../template/cover/template_cover.h | 9 ++-- .../template/datetime/template_date.cpp | 15 +++--- .../template/datetime/template_date.h | 5 +- .../template/datetime/template_datetime.cpp | 21 ++++---- .../template/datetime/template_datetime.h | 5 +- .../template/datetime/template_time.cpp | 15 +++--- .../template/datetime/template_time.h | 5 +- .../template/lock/template_lock.cpp | 15 +++--- .../components/template/lock/template_lock.h | 6 ++- .../template/number/template_number.cpp | 9 ++-- .../template/number/template_number.h | 5 +- .../template/select/template_select.cpp | 16 +++--- .../template/select/template_select.h | 5 +- .../template/sensor/template_sensor.cpp | 5 +- .../template/sensor/template_sensor.h | 5 +- .../template/switch/template_switch.cpp | 15 +++--- .../template/switch/template_switch.h | 5 +- .../template/text/template_text.cpp | 18 +++---- .../components/template/text/template_text.h | 5 +- .../text_sensor/template_text_sensor.cpp | 5 +- .../text_sensor/template_text_sensor.h | 5 +- .../template/valve/template_valve.cpp | 17 +++---- .../template/valve/template_valve.h | 5 +- esphome/core/template_lambda.h | 51 +++++++++++++++++++ tests/components/template/common-base.yaml | 8 +++ tests/integration/fixtures/runtime_stats.yaml | 1 + 29 files changed, 196 insertions(+), 132 deletions(-) create mode 100644 esphome/core/template_lambda.h diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.cpp b/esphome/components/template/binary_sensor/template_binary_sensor.cpp index 8543dff4dc..806aed49b1 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.cpp +++ b/esphome/components/template/binary_sensor/template_binary_sensor.cpp @@ -6,17 +6,21 @@ namespace template_ { static const char *const TAG = "template.binary_sensor"; -void TemplateBinarySensor::setup() { this->loop(); } +void TemplateBinarySensor::setup() { + if (!this->f_.has_value()) { + this->disable_loop(); + } else { + this->loop(); + } +} void TemplateBinarySensor::loop() { - if (!this->f_.has_value()) - return; - - auto s = (*this->f_)(); + auto s = this->f_(); if (s.has_value()) { this->publish_state(*s); } } + void TemplateBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Template Binary Sensor", this); } } // namespace template_ diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.h b/esphome/components/template/binary_sensor/template_binary_sensor.h index 2e0b216eb4..bc591391b9 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.h +++ b/esphome/components/template/binary_sensor/template_binary_sensor.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/binary_sensor/binary_sensor.h" namespace esphome { @@ -8,7 +9,7 @@ namespace template_ { class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor { public: - void set_template(optional (*f)()) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void loop() override; @@ -17,7 +18,7 @@ class TemplateBinarySensor : public Component, public binary_sensor::BinarySenso float get_setup_priority() const override { return setup_priority::HARDWARE; } protected: - optional (*)()> f_; + TemplateLambda f_; }; } // namespace template_ diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index bed3931e78..a87f28ccec 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -33,28 +33,27 @@ void TemplateCover::setup() { break; } } + if (!this->state_f_.has_value() && !this->tilt_f_.has_value()) + this->disable_loop(); } void TemplateCover::loop() { bool changed = false; - if (this->state_f_.has_value()) { - auto s = (*this->state_f_)(); - if (s.has_value()) { - auto pos = clamp(*s, 0.0f, 1.0f); - if (pos != this->position) { - this->position = pos; - changed = true; - } + auto s = this->state_f_(); + if (s.has_value()) { + auto pos = clamp(*s, 0.0f, 1.0f); + if (pos != this->position) { + this->position = pos; + changed = true; } } - if (this->tilt_f_.has_value()) { - auto s = (*this->tilt_f_)(); - if (s.has_value()) { - auto tilt = clamp(*s, 0.0f, 1.0f); - if (tilt != this->tilt) { - this->tilt = tilt; - changed = true; - } + + auto tilt = this->tilt_f_(); + if (tilt.has_value()) { + auto tilt_val = clamp(*tilt, 0.0f, 1.0f); + if (tilt_val != this->tilt) { + this->tilt = tilt_val; + changed = true; } } @@ -63,7 +62,6 @@ void TemplateCover::loop() { } void TemplateCover::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void TemplateCover::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } -void TemplateCover::set_state_lambda(optional (*f)()) { this->state_f_ = f; } float TemplateCover::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; } Trigger<> *TemplateCover::get_close_trigger() const { return this->close_trigger_; } @@ -124,7 +122,6 @@ CoverTraits TemplateCover::get_traits() { } Trigger *TemplateCover::get_position_trigger() const { return this->position_trigger_; } Trigger *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; } -void TemplateCover::set_tilt_lambda(optional (*tilt_f)()) { this->tilt_f_ = tilt_f; } void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } void TemplateCover::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index ed1ebf4e43..faff69f867 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/cover/cover.h" namespace esphome { @@ -17,7 +18,8 @@ class TemplateCover : public cover::Cover, public Component { public: TemplateCover(); - void set_state_lambda(optional (*f)()); + template void set_state_lambda(F &&f) { this->state_f_.set(std::forward(f)); } + template void set_tilt_lambda(F &&f) { this->tilt_f_.set(std::forward(f)); } Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; @@ -26,7 +28,6 @@ class TemplateCover : public cover::Cover, public Component { Trigger *get_tilt_trigger() const; void set_optimistic(bool optimistic); void set_assumed_state(bool assumed_state); - void set_tilt_lambda(optional (*tilt_f)()); void set_has_stop(bool has_stop); void set_has_position(bool has_position); void set_has_tilt(bool has_tilt); @@ -45,8 +46,8 @@ class TemplateCover : public cover::Cover, public Component { void stop_prev_trigger_(); TemplateCoverRestoreMode restore_mode_{COVER_RESTORE}; - optional (*)()> state_f_; - optional (*)()> tilt_f_; + TemplateLambda state_f_; + TemplateLambda tilt_f_; bool assumed_state_{false}; bool optimistic_{false}; Trigger<> *open_trigger_; diff --git a/esphome/components/template/datetime/template_date.cpp b/esphome/components/template/datetime/template_date.cpp index 2fa8016802..3f6626e847 100644 --- a/esphome/components/template/datetime/template_date.cpp +++ b/esphome/components/template/datetime/template_date.cpp @@ -40,14 +40,13 @@ void TemplateDate::update() { if (!this->f_.has_value()) return; - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - this->year_ = val->year; - this->month_ = val->month; - this->day_ = val->day_of_month; - this->publish_state(); + auto val = this->f_(); + if (val.has_value()) { + this->year_ = val->year; + this->month_ = val->month; + this->day_ = val->day_of_month; + this->publish_state(); + } } void TemplateDate::control(const datetime::DateCall &call) { diff --git a/esphome/components/template/datetime/template_date.h b/esphome/components/template/datetime/template_date.h index 2a0967fc94..7fed704d0e 100644 --- a/esphome/components/template/datetime/template_date.h +++ b/esphome/components/template/datetime/template_date.h @@ -9,13 +9,14 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/time.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { class TemplateDate : public datetime::DateEntity, public PollingComponent { public: - void set_template(optional (*f)()) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void update() override; @@ -35,7 +36,7 @@ class TemplateDate : public datetime::DateEntity, public PollingComponent { ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional (*)()> f_; + TemplateLambda f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/datetime/template_datetime.cpp b/esphome/components/template/datetime/template_datetime.cpp index a4a4e47d65..62f842a7ad 100644 --- a/esphome/components/template/datetime/template_datetime.cpp +++ b/esphome/components/template/datetime/template_datetime.cpp @@ -43,17 +43,16 @@ void TemplateDateTime::update() { if (!this->f_.has_value()) return; - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - this->year_ = val->year; - this->month_ = val->month; - this->day_ = val->day_of_month; - this->hour_ = val->hour; - this->minute_ = val->minute; - this->second_ = val->second; - this->publish_state(); + auto val = this->f_(); + if (val.has_value()) { + this->year_ = val->year; + this->month_ = val->month; + this->day_ = val->day_of_month; + this->hour_ = val->hour; + this->minute_ = val->minute; + this->second_ = val->second; + this->publish_state(); + } } void TemplateDateTime::control(const datetime::DateTimeCall &call) { diff --git a/esphome/components/template/datetime/template_datetime.h b/esphome/components/template/datetime/template_datetime.h index d917015b67..ec45bf0326 100644 --- a/esphome/components/template/datetime/template_datetime.h +++ b/esphome/components/template/datetime/template_datetime.h @@ -9,13 +9,14 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/time.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent { public: - void set_template(optional (*f)()) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void update() override; @@ -35,7 +36,7 @@ class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponen ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional (*)()> f_; + TemplateLambda f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/datetime/template_time.cpp b/esphome/components/template/datetime/template_time.cpp index 349700f187..dab28d01cc 100644 --- a/esphome/components/template/datetime/template_time.cpp +++ b/esphome/components/template/datetime/template_time.cpp @@ -40,14 +40,13 @@ void TemplateTime::update() { if (!this->f_.has_value()) return; - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - this->hour_ = val->hour; - this->minute_ = val->minute; - this->second_ = val->second; - this->publish_state(); + auto val = this->f_(); + if (val.has_value()) { + this->hour_ = val->hour; + this->minute_ = val->minute; + this->second_ = val->second; + this->publish_state(); + } } void TemplateTime::control(const datetime::TimeCall &call) { diff --git a/esphome/components/template/datetime/template_time.h b/esphome/components/template/datetime/template_time.h index 2f05ba0737..ea7474c0ba 100644 --- a/esphome/components/template/datetime/template_time.h +++ b/esphome/components/template/datetime/template_time.h @@ -9,13 +9,14 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/time.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { class TemplateTime : public datetime::TimeEntity, public PollingComponent { public: - void set_template(optional (*f)()) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void update() override; @@ -35,7 +36,7 @@ class TemplateTime : public datetime::TimeEntity, public PollingComponent { ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional (*)()> f_; + TemplateLambda f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/lock/template_lock.cpp b/esphome/components/template/lock/template_lock.cpp index c2e227c26d..8ed87b9736 100644 --- a/esphome/components/template/lock/template_lock.cpp +++ b/esphome/components/template/lock/template_lock.cpp @@ -11,14 +11,16 @@ static const char *const TAG = "template.lock"; TemplateLock::TemplateLock() : lock_trigger_(new Trigger<>()), unlock_trigger_(new Trigger<>()), open_trigger_(new Trigger<>()) {} -void TemplateLock::loop() { +void TemplateLock::setup() { if (!this->f_.has_value()) - return; - auto val = (*this->f_)(); - if (!val.has_value()) - return; + this->disable_loop(); +} - this->publish_state(*val); +void TemplateLock::loop() { + auto val = this->f_(); + if (val.has_value()) { + this->publish_state(*val); + } } void TemplateLock::control(const lock::LockCall &call) { if (this->prev_trigger_ != nullptr) { @@ -45,7 +47,6 @@ void TemplateLock::open_latch() { this->open_trigger_->trigger(); } void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } -void TemplateLock::set_state_lambda(optional (*f)()) { this->f_ = f; } float TemplateLock::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateLock::get_lock_trigger() const { return this->lock_trigger_; } Trigger<> *TemplateLock::get_unlock_trigger() const { return this->unlock_trigger_; } diff --git a/esphome/components/template/lock/template_lock.h b/esphome/components/template/lock/template_lock.h index 428744a66f..14fca4635e 100644 --- a/esphome/components/template/lock/template_lock.h +++ b/esphome/components/template/lock/template_lock.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/lock/lock.h" namespace esphome { @@ -11,9 +12,10 @@ class TemplateLock : public lock::Lock, public Component { public: TemplateLock(); + void setup() override; void dump_config() override; - void set_state_lambda(optional (*f)()); + template void set_state_lambda(F &&f) { this->f_.set(std::forward(f)); } Trigger<> *get_lock_trigger() const; Trigger<> *get_unlock_trigger() const; Trigger<> *get_open_trigger() const; @@ -26,7 +28,7 @@ class TemplateLock : public lock::Lock, public Component { void control(const lock::LockCall &call) override; void open_latch() override; - optional (*)()> f_; + TemplateLambda f_; bool optimistic_{false}; Trigger<> *lock_trigger_; Trigger<> *unlock_trigger_; diff --git a/esphome/components/template/number/template_number.cpp b/esphome/components/template/number/template_number.cpp index 187f426273..145a89a2f7 100644 --- a/esphome/components/template/number/template_number.cpp +++ b/esphome/components/template/number/template_number.cpp @@ -30,11 +30,10 @@ void TemplateNumber::update() { if (!this->f_.has_value()) return; - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - this->publish_state(*val); + auto val = this->f_(); + if (val.has_value()) { + this->publish_state(*val); + } } void TemplateNumber::control(float value) { diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h index e77b181d25..a9307e9246 100644 --- a/esphome/components/template/number/template_number.h +++ b/esphome/components/template/number/template_number.h @@ -4,13 +4,14 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { class TemplateNumber : public number::Number, public PollingComponent { public: - void set_template(optional (*f)()) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void update() override; @@ -28,7 +29,7 @@ class TemplateNumber : public number::Number, public PollingComponent { float initial_value_{NAN}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional (*)()> f_; + TemplateLambda f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index c7a1d8a344..3ea34c3c7c 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -31,16 +31,14 @@ void TemplateSelect::update() { if (!this->f_.has_value()) return; - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - if (!this->has_option(*val)) { - ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str()); - return; + auto val = this->f_(); + if (val.has_value()) { + if (!this->has_option(*val)) { + ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str()); + return; + } + this->publish_state(*val); } - - this->publish_state(*val); } void TemplateSelect::control(const std::string &value) { diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index e77e4d8f14..1c33153872 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -4,13 +4,14 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { class TemplateSelect : public select::Select, public PollingComponent { public: - void set_template(optional (*f)()) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void update() override; @@ -28,7 +29,7 @@ class TemplateSelect : public select::Select, public PollingComponent { size_t initial_option_index_{0}; bool restore_value_ = false; Trigger *set_trigger_ = new Trigger(); - optional (*)()> f_; + TemplateLambda f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/sensor/template_sensor.cpp b/esphome/components/template/sensor/template_sensor.cpp index 65f2417670..1558ea9b15 100644 --- a/esphome/components/template/sensor/template_sensor.cpp +++ b/esphome/components/template/sensor/template_sensor.cpp @@ -11,13 +11,14 @@ void TemplateSensor::update() { if (!this->f_.has_value()) return; - auto val = (*this->f_)(); + auto val = this->f_(); if (val.has_value()) { this->publish_state(*val); } } + float TemplateSensor::get_setup_priority() const { return setup_priority::HARDWARE; } -void TemplateSensor::set_template(optional (*f)()) { this->f_ = f; } + void TemplateSensor::dump_config() { LOG_SENSOR("", "Template Sensor", this); LOG_UPDATE_INTERVAL(this); diff --git a/esphome/components/template/sensor/template_sensor.h b/esphome/components/template/sensor/template_sensor.h index 369313d607..793d754a0f 100644 --- a/esphome/components/template/sensor/template_sensor.h +++ b/esphome/components/template/sensor/template_sensor.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/sensor/sensor.h" namespace esphome { @@ -8,7 +9,7 @@ namespace template_ { class TemplateSensor : public sensor::Sensor, public PollingComponent { public: - void set_template(optional (*f)()); + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void update() override; @@ -17,7 +18,7 @@ class TemplateSensor : public sensor::Sensor, public PollingComponent { float get_setup_priority() const override; protected: - optional (*)()> f_; + TemplateLambda f_; }; } // namespace template_ diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index 5aaf514b2a..95e8692da5 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -9,13 +9,10 @@ static const char *const TAG = "template.switch"; TemplateSwitch::TemplateSwitch() : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {} void TemplateSwitch::loop() { - if (!this->f_.has_value()) - return; - auto s = (*this->f_)(); - if (!s.has_value()) - return; - - this->publish_state(*s); + auto s = this->f_(); + if (s.has_value()) { + this->publish_state(*s); + } } void TemplateSwitch::write_state(bool state) { if (this->prev_trigger_ != nullptr) { @@ -35,11 +32,13 @@ void TemplateSwitch::write_state(bool state) { } void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } bool TemplateSwitch::assumed_state() { return this->assumed_state_; } -void TemplateSwitch::set_state_lambda(optional (*f)()) { this->f_ = f; } float TemplateSwitch::get_setup_priority() const { return setup_priority::HARDWARE - 2.0f; } Trigger<> *TemplateSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } Trigger<> *TemplateSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } void TemplateSwitch::setup() { + if (!this->f_.has_value()) + this->disable_loop(); + optional initial_state = this->get_initial_state_with_restore_mode(); if (initial_state.has_value()) { diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index 0fba66b9bd..18a374df35 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/switch/switch.h" namespace esphome { @@ -14,7 +15,7 @@ class TemplateSwitch : public switch_::Switch, public Component { void setup() override; void dump_config() override; - void set_state_lambda(optional (*f)()); + template void set_state_lambda(F &&f) { this->f_.set(std::forward(f)); } Trigger<> *get_turn_on_trigger() const; Trigger<> *get_turn_off_trigger() const; void set_optimistic(bool optimistic); @@ -28,7 +29,7 @@ class TemplateSwitch : public switch_::Switch, public Component { void write_state(bool state) override; - optional (*)()> f_; + TemplateLambda f_; bool optimistic_{false}; bool assumed_state_{false}; Trigger<> *turn_on_trigger_; diff --git a/esphome/components/template/text/template_text.cpp b/esphome/components/template/text/template_text.cpp index d8e840ba7e..a917c72a14 100644 --- a/esphome/components/template/text/template_text.cpp +++ b/esphome/components/template/text/template_text.cpp @@ -7,10 +7,8 @@ namespace template_ { static const char *const TAG = "template.text"; void TemplateText::setup() { - if (!(this->f_ == nullptr)) { - if (this->f_.has_value()) - return; - } + if (this->f_.has_value()) + return; std::string value = this->initial_value_; if (!this->pref_) { ESP_LOGD(TAG, "State from initial: %s", value.c_str()); @@ -26,17 +24,13 @@ void TemplateText::setup() { } void TemplateText::update() { - if (this->f_ == nullptr) - return; - if (!this->f_.has_value()) return; - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - this->publish_state(*val); + auto val = this->f_(); + if (val.has_value()) { + this->publish_state(*val); + } } void TemplateText::control(const std::string &value) { diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index 6c17d2016a..c12021f80e 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -4,6 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { @@ -61,7 +62,7 @@ template class TextSaver : public TemplateTextSaverBase { class TemplateText : public text::Text, public PollingComponent { public: - void set_template(optional (*f)()) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void update() override; @@ -78,7 +79,7 @@ class TemplateText : public text::Text, public PollingComponent { bool optimistic_ = false; std::string initial_value_; Trigger *set_trigger_ = new Trigger(); - optional (*)()> f_{nullptr}; + TemplateLambda f_{}; TemplateTextSaverBase *pref_ = nullptr; }; diff --git a/esphome/components/template/text_sensor/template_text_sensor.cpp b/esphome/components/template/text_sensor/template_text_sensor.cpp index 2b0297d62f..024d0093a2 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.cpp +++ b/esphome/components/template/text_sensor/template_text_sensor.cpp @@ -10,13 +10,14 @@ void TemplateTextSensor::update() { if (!this->f_.has_value()) return; - auto val = (*this->f_)(); + auto val = this->f_(); if (val.has_value()) { this->publish_state(*val); } } + float TemplateTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } -void TemplateTextSensor::set_template(optional (*f)()) { this->f_ = f; } + void TemplateTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Template Sensor", this); } } // namespace template_ diff --git a/esphome/components/template/text_sensor/template_text_sensor.h b/esphome/components/template/text_sensor/template_text_sensor.h index 48e40c2493..0d01c72023 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.h +++ b/esphome/components/template/text_sensor/template_text_sensor.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/text_sensor/text_sensor.h" namespace esphome { @@ -9,7 +10,7 @@ namespace template_ { class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent { public: - void set_template(optional (*f)()); + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void update() override; @@ -18,7 +19,7 @@ class TemplateTextSensor : public text_sensor::TextSensor, public PollingCompone void dump_config() override; protected: - optional (*)()> f_{}; + TemplateLambda f_{}; }; } // namespace template_ diff --git a/esphome/components/template/valve/template_valve.cpp b/esphome/components/template/valve/template_valve.cpp index b27cc00968..b91b32473e 100644 --- a/esphome/components/template/valve/template_valve.cpp +++ b/esphome/components/template/valve/template_valve.cpp @@ -33,19 +33,19 @@ void TemplateValve::setup() { break; } } + if (!this->state_f_.has_value()) + this->disable_loop(); } void TemplateValve::loop() { bool changed = false; - if (this->state_f_.has_value()) { - auto s = (*this->state_f_)(); - if (s.has_value()) { - auto pos = clamp(*s, 0.0f, 1.0f); - if (pos != this->position) { - this->position = pos; - changed = true; - } + auto s = this->state_f_(); + if (s.has_value()) { + auto pos = clamp(*s, 0.0f, 1.0f); + if (pos != this->position) { + this->position = pos; + changed = true; } } @@ -55,7 +55,6 @@ void TemplateValve::loop() { void TemplateValve::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void TemplateValve::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } -void TemplateValve::set_state_lambda(optional (*f)()) { this->state_f_ = f; } float TemplateValve::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateValve::get_open_trigger() const { return this->open_trigger_; } diff --git a/esphome/components/template/valve/template_valve.h b/esphome/components/template/valve/template_valve.h index 92c32f3487..d6235f8e5c 100644 --- a/esphome/components/template/valve/template_valve.h +++ b/esphome/components/template/valve/template_valve.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/valve/valve.h" namespace esphome { @@ -17,7 +18,7 @@ class TemplateValve : public valve::Valve, public Component { public: TemplateValve(); - void set_state_lambda(optional (*f)()); + template void set_state_lambda(F &&f) { this->state_f_.set(std::forward(f)); } Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; @@ -42,7 +43,7 @@ class TemplateValve : public valve::Valve, public Component { void stop_prev_trigger_(); TemplateValveRestoreMode restore_mode_{VALVE_NO_RESTORE}; - optional (*)()> state_f_; + TemplateLambda state_f_; bool assumed_state_{false}; bool optimistic_{false}; Trigger<> *open_trigger_; diff --git a/esphome/core/template_lambda.h b/esphome/core/template_lambda.h new file mode 100644 index 0000000000..7b8f4374aa --- /dev/null +++ b/esphome/core/template_lambda.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/core/optional.h" + +namespace esphome { + +/** Lightweight wrapper for template platform lambdas (stateless function pointers only). + * + * This optimizes template platforms by storing only a function pointer (4 bytes on ESP32) + * instead of std::function (16-32 bytes). + * + * IMPORTANT: This only supports stateless lambdas (no captures). The set_template() method + * is an internal API used by YAML codegen, not intended for external use. + * + * Lambdas must return optional to support the pattern: + * return {}; // Don't publish a value + * return 42.0; // Publish this value + * + * operator() returns optional, returning nullopt when no lambda is set (nullptr check). + * + * @tparam T The return type (e.g., float for sensor values) + * @tparam Args Optional arguments for the lambda + */ +template class TemplateLambda { + public: + TemplateLambda() : f_(nullptr) {} + + /** Set the lambda function pointer. + * INTERNAL API: Only for use by YAML codegen. + * Only stateless lambdas (no captures) are supported. + */ + void set(optional (*f)(Args...)) { this->f_ = f; } + + /** Check if a lambda is set */ + bool has_value() const { return this->f_ != nullptr; } + + /** Call the lambda, returning nullopt if no lambda is set */ + optional operator()(Args &&...args) { + if (this->f_ == nullptr) + return nullopt; + return this->f_(std::forward(args)...); + } + + /** Alias for operator() for compatibility */ + optional call(Args &&...args) { return (*this)(std::forward(args)...); } + + protected: + optional (*f_)(Args...); // Function pointer (4 bytes on ESP32) +}; + +} // namespace esphome diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index b873af5207..f101eea942 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -9,6 +9,13 @@ esphome: id: template_sens state: !lambda "return 42.0;" + # Test C++ API: set_template() with stateless lambda (no captures) + # NOTE: set_template() is not intended to be a public API, but we test it to ensure it doesn't break. + - lambda: |- + id(template_sens).set_template([]() -> esphome::optional { + return 123.0f; + }); + - datetime.date.set: id: test_date date: @@ -215,6 +222,7 @@ cover: number: - platform: template + id: template_number name: "Template number" optimistic: true min_value: 0 diff --git a/tests/integration/fixtures/runtime_stats.yaml b/tests/integration/fixtures/runtime_stats.yaml index aad1c275fb..fd34cdb939 100644 --- a/tests/integration/fixtures/runtime_stats.yaml +++ b/tests/integration/fixtures/runtime_stats.yaml @@ -32,6 +32,7 @@ switch: name: "Test Switch" id: test_switch optimistic: true + lambda: return false; interval: - interval: 0.5s From bd87e56bc7bbdb2a1d7178536bd9f1c5b9232d45 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 21:14:03 -0500 Subject: [PATCH 240/394] [e131] Replace std::set with std::vector to reduce flash usage (#11598) --- esphome/components/e131/e131.cpp | 13 +++++++++---- esphome/components/e131/e131.h | 4 +--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index d18d945cec..c10c88faf2 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -3,6 +3,8 @@ #include "e131_addressable_light_effect.h" #include "esphome/core/log.h" +#include + namespace esphome { namespace e131 { @@ -76,14 +78,14 @@ void E131Component::loop() { } void E131Component::add_effect(E131AddressableLightEffect *light_effect) { - if (light_effects_.count(light_effect)) { + if (std::find(light_effects_.begin(), light_effects_.end(), light_effect) != light_effects_.end()) { return; } ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(), light_effect->get_last_universe()); - light_effects_.insert(light_effect); + light_effects_.push_back(light_effect); for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { join_(universe); @@ -91,14 +93,17 @@ void E131Component::add_effect(E131AddressableLightEffect *light_effect) { } void E131Component::remove_effect(E131AddressableLightEffect *light_effect) { - if (!light_effects_.count(light_effect)) { + auto it = std::find(light_effects_.begin(), light_effects_.end(), light_effect); + if (it == light_effects_.end()) { return; } ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(), light_effect->get_last_universe()); - light_effects_.erase(light_effect); + // Swap with last element and pop for O(1) removal (order doesn't matter) + *it = light_effects_.back(); + light_effects_.pop_back(); for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { leave_(universe); diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index d0e38fa98c..831138a545 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -7,7 +7,6 @@ #include #include #include -#include #include namespace esphome { @@ -47,9 +46,8 @@ class E131Component : public esphome::Component { E131ListenMethod listen_method_{E131_MULTICAST}; std::unique_ptr socket_; - std::set light_effects_; + std::vector light_effects_; std::map universe_consumers_; - std::map universe_packets_; }; } // namespace e131 From 077cce9848303422581a0616b875d53044369a8c Mon Sep 17 00:00:00 2001 From: Markus <974709+Links2004@users.noreply.github.com> Date: Thu, 30 Oct 2025 16:08:08 +0100 Subject: [PATCH 241/394] [core] .local addresses are only resolvable if mDNS is enabled (#11508) --- esphome/__main__.py | 16 +++++++++++++--- tests/unit_tests/test_main.py | 6 +++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 26e5ae7424..b0c081a34f 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -207,14 +207,14 @@ def choose_upload_log_host( if has_mqtt_logging(): resolved.append("MQTT") - if has_api() and has_non_ip_address(): + if has_api() and has_non_ip_address() and has_resolvable_address(): resolved.extend(_resolve_with_cache(CORE.address, purpose)) elif purpose == Purpose.UPLOADING: if has_ota() and has_mqtt_ip_lookup(): resolved.append("MQTTIP") - if has_ota() and has_non_ip_address(): + if has_ota() and has_non_ip_address() and has_resolvable_address(): resolved.extend(_resolve_with_cache(CORE.address, purpose)) else: resolved.append(device) @@ -318,7 +318,17 @@ def has_resolvable_address() -> bool: """Check if CORE.address is resolvable (via mDNS, DNS, or is an IP address).""" # Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable # The resolve_ip_address() function in helpers.py handles all types via AsyncResolver - return CORE.address is not None + if CORE.address is None: + return False + + if has_ip_address(): + return True + + if has_mdns(): + return True + + # .local mDNS hostnames are only resolvable if mDNS is enabled + return not CORE.address.endswith(".local") def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str): diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 9119c88502..9e5f399381 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -744,7 +744,7 @@ def test_choose_upload_log_host_ota_local_all_options() -> None: check_default=None, purpose=Purpose.UPLOADING, ) - assert result == ["MQTTIP", "test.local"] + assert result == ["MQTTIP"] @pytest.mark.usefixtures("mock_serial_ports") @@ -794,7 +794,7 @@ def test_choose_upload_log_host_ota_local_all_options_logging() -> None: check_default=None, purpose=Purpose.LOGGING, ) - assert result == ["MQTTIP", "MQTT", "test.local"] + assert result == ["MQTTIP", "MQTT"] @pytest.mark.usefixtures("mock_no_mqtt_logging") @@ -1564,7 +1564,7 @@ def test_has_resolvable_address() -> None: setup_core( config={CONF_MDNS: {CONF_DISABLED: True}}, address="esphome-device.local" ) - assert has_resolvable_address() is True + assert has_resolvable_address() is False # Test with mDNS disabled and regular DNS hostname (resolvable) setup_core(config={CONF_MDNS: {CONF_DISABLED: True}}, address="device.example.com") From d94c7b9c12adee6f4f65c818bffaadcc000cb38b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 10:20:21 -0500 Subject: [PATCH 242/394] [climate] Replace std::vector with const char* for custom fan modes and presets --- esphome/components/api/api.proto | 4 +- esphome/components/api/api_pb2.cpp | 16 +++--- esphome/components/api/api_pb2.h | 4 +- .../bedjet/climate/bedjet_climate.h | 12 +---- esphome/components/climate/climate.cpp | 24 ++++----- esphome/components/climate/climate_traits.h | 54 +++++++++---------- esphome/components/midea/ac_adapter.cpp | 12 ++--- esphome/components/midea/ac_adapter.h | 10 ++-- esphome/components/midea/air_conditioner.cpp | 6 ++- esphome/components/midea/air_conditioner.h | 8 +-- script/api_protobuf/api_protobuf.py | 3 +- 11 files changed, 71 insertions(+), 82 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index f50944ffa4..cae8b23c5f 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1000,9 +1000,9 @@ message ListEntitiesClimateResponse { bool supports_action = 12; // Deprecated: use feature_flags repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer_no_template) = "climate::ClimateFanModeMask"]; repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"]; - repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::vector"]; + repeated string supported_custom_fan_modes = 15 [(container_pointer_no_template) = "std::vector"]; repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"]; - repeated string supported_custom_presets = 17 [(container_pointer) = "std::vector"]; + repeated string supported_custom_presets = 17 [(container_pointer_no_template) = "std::vector"]; bool disabled_by_default = 18; string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 20; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 3472707d3c..ba8bcf3275 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1179,14 +1179,14 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { for (const auto &it : *this->supported_swing_modes) { buffer.encode_uint32(14, static_cast(it), true); } - for (const auto &it : *this->supported_custom_fan_modes) { - buffer.encode_string(15, it, true); + for (const char *it : *this->supported_custom_fan_modes) { + buffer.encode_string(15, it, strlen(it), true); } for (const auto &it : *this->supported_presets) { buffer.encode_uint32(16, static_cast(it), true); } - for (const auto &it : *this->supported_custom_presets) { - buffer.encode_string(17, it, true); + for (const char *it : *this->supported_custom_presets) { + buffer.encode_string(17, it, strlen(it), true); } buffer.encode_bool(18, this->disabled_by_default); #ifdef USE_ENTITY_ICON @@ -1229,8 +1229,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const { } } if (!this->supported_custom_fan_modes->empty()) { - for (const auto &it : *this->supported_custom_fan_modes) { - size.add_length_force(1, it.size()); + for (const char *it : *this->supported_custom_fan_modes) { + size.add_length_force(1, strlen(it)); } } if (!this->supported_presets->empty()) { @@ -1239,8 +1239,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const { } } if (!this->supported_custom_presets->empty()) { - for (const auto &it : *this->supported_custom_presets) { - size.add_length_force(2, it.size()); + for (const char *it : *this->supported_custom_presets) { + size.add_length_force(2, strlen(it)); } } size.add_bool(2, this->disabled_by_default); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index aa5c031ac7..7419508621 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1384,9 +1384,9 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { bool supports_action{false}; const climate::ClimateFanModeMask *supported_fan_modes{}; const climate::ClimateSwingModeMask *supported_swing_modes{}; - const std::vector *supported_custom_fan_modes{}; + const std::vector *supported_custom_fan_modes{}; const climate::ClimatePresetMask *supported_presets{}; - const std::vector *supported_custom_presets{}; + const std::vector *supported_custom_presets{}; float visual_current_temperature_step{0.0f}; bool supports_current_humidity{false}; bool supports_target_humidity{false}; diff --git a/esphome/components/bedjet/climate/bedjet_climate.h b/esphome/components/bedjet/climate/bedjet_climate.h index dbbb73aeae..05f4a849e0 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.h +++ b/esphome/components/bedjet/climate/bedjet_climate.h @@ -50,21 +50,13 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli // Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead. climate::CLIMATE_PRESET_BOOST, }); + // String literals are stored in rodata and valid for program lifetime traits.set_supported_custom_presets({ - // We could fetch biodata from bedjet and set these names that way. - // But then we have to invert the lookup in order to send the right preset. - // For now, we can leave them as M1-3 to match the remote buttons. - // EXT HT added to match remote button. - "EXT HT", + this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT", "M1", "M2", "M3", }); - if (this->heating_mode_ == HEAT_MODE_EXTENDED) { - traits.add_supported_custom_preset("LTD HT"); - } else { - traits.add_supported_custom_preset("EXT HT"); - } traits.set_visual_min_temperature(19.0); traits.set_visual_max_temperature(43.0); traits.set_visual_temperature_step(1.0); diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 944934edbf..64f43ffd80 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -387,8 +387,8 @@ void Climate::save_state_() { const auto &supported = traits.get_supported_custom_fan_modes(); // std::vector maintains insertion order size_t i = 0; - for (const auto &mode : supported) { - if (mode == custom_fan_mode) { + for (const char *mode : supported) { + if (strcmp(mode, custom_fan_mode.value().c_str()) == 0) { state.custom_fan_mode = i; break; } @@ -404,8 +404,8 @@ void Climate::save_state_() { const auto &supported = traits.get_supported_custom_presets(); // std::vector maintains insertion order size_t i = 0; - for (const auto &preset : supported) { - if (preset == custom_preset) { + for (const char *preset : supported) { + if (strcmp(preset, custom_preset.value().c_str()) == 0) { state.custom_preset = i; break; } @@ -527,7 +527,7 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { if (this->uses_custom_fan_mode) { if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { call.fan_mode_.reset(); - call.custom_fan_mode_ = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode); + call.custom_fan_mode_ = std::string(traits.get_supported_custom_fan_modes()[this->custom_fan_mode]); } } else if (traits.supports_fan_mode(this->fan_mode)) { call.set_fan_mode(this->fan_mode); @@ -535,7 +535,7 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { if (this->uses_custom_preset) { if (this->custom_preset < traits.get_supported_custom_presets().size()) { call.preset_.reset(); - call.custom_preset_ = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset); + call.custom_preset_ = std::string(traits.get_supported_custom_presets()[this->custom_preset]); } } else if (traits.supports_preset(this->preset)) { call.set_preset(this->preset); @@ -562,7 +562,7 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { if (this->uses_custom_fan_mode) { if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { climate->fan_mode.reset(); - climate->custom_fan_mode = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode); + climate->custom_fan_mode = std::string(traits.get_supported_custom_fan_modes()[this->custom_fan_mode]); } } else if (traits.supports_fan_mode(this->fan_mode)) { climate->fan_mode = this->fan_mode; @@ -571,7 +571,7 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { if (this->uses_custom_preset) { if (this->custom_preset < traits.get_supported_custom_presets().size()) { climate->preset.reset(); - climate->custom_preset = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset); + climate->custom_preset = std::string(traits.get_supported_custom_presets()[this->custom_preset]); } } else if (traits.supports_preset(this->preset)) { climate->preset = this->preset; @@ -656,8 +656,8 @@ void Climate::dump_traits_(const char *tag) { } if (!traits.get_supported_custom_fan_modes().empty()) { ESP_LOGCONFIG(tag, " Supported custom fan modes:"); - for (const std::string &s : traits.get_supported_custom_fan_modes()) - ESP_LOGCONFIG(tag, " - %s", s.c_str()); + for (const char *s : traits.get_supported_custom_fan_modes()) + ESP_LOGCONFIG(tag, " - %s", s); } if (!traits.get_supported_presets().empty()) { ESP_LOGCONFIG(tag, " Supported presets:"); @@ -666,8 +666,8 @@ void Climate::dump_traits_(const char *tag) { } if (!traits.get_supported_custom_presets().empty()) { ESP_LOGCONFIG(tag, " Supported custom presets:"); - for (const std::string &s : traits.get_supported_custom_presets()) - ESP_LOGCONFIG(tag, " - %s", s.c_str()); + for (const char *s : traits.get_supported_custom_presets()) + ESP_LOGCONFIG(tag, " - %s", s); } if (!traits.get_supported_swing_modes().empty()) { ESP_LOGCONFIG(tag, " Supported swing modes:"); diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 1161a54f4e..b9789d9ccb 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include "climate_mode.h" #include "esphome/core/finite_set_mask.h" @@ -18,16 +19,6 @@ using ClimateSwingModeMask = FiniteSetMask>; using ClimatePresetMask = FiniteSetMask>; -// Lightweight linear search for small vectors (1-20 items) -// Avoids std::find template overhead -template inline bool vector_contains(const std::vector &vec, const T &value) { - for (const auto &item : vec) { - if (item == value) - return true; - } - return false; -} - /** This class contains all static data for climate devices. * * All climate devices must support these features: @@ -128,46 +119,46 @@ class ClimateTraits { void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; } void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); } - void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.push_back(mode); } bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); } bool get_supports_fan_modes() const { return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty(); } const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; } - void set_supported_custom_fan_modes(std::vector supported_custom_fan_modes) { - this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes); - } - void set_supported_custom_fan_modes(std::initializer_list modes) { + void set_supported_custom_fan_modes(std::initializer_list modes) { this->supported_custom_fan_modes_ = modes; } - template void set_supported_custom_fan_modes(const char *const (&modes)[N]) { - this->supported_custom_fan_modes_.assign(modes, modes + N); + void set_supported_custom_fan_modes(const std::vector &modes) { + this->supported_custom_fan_modes_ = modes; } - const std::vector &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } + const std::vector &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { - return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode); + for (const char *mode : this->supported_custom_fan_modes_) { + if (strcmp(mode, custom_fan_mode.c_str()) == 0) + return true; + } + return false; } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); } - void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.push_back(preset); } bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); } bool get_supports_presets() const { return !this->supported_presets_.empty(); } const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; } - void set_supported_custom_presets(std::vector supported_custom_presets) { - this->supported_custom_presets_ = std::move(supported_custom_presets); - } - void set_supported_custom_presets(std::initializer_list presets) { + void set_supported_custom_presets(std::initializer_list presets) { this->supported_custom_presets_ = presets; } - template void set_supported_custom_presets(const char *const (&presets)[N]) { - this->supported_custom_presets_.assign(presets, presets + N); + void set_supported_custom_presets(const std::vector &presets) { + this->supported_custom_presets_ = presets; } - const std::vector &get_supported_custom_presets() const { return this->supported_custom_presets_; } + const std::vector &get_supported_custom_presets() const { return this->supported_custom_presets_; } bool supports_custom_preset(const std::string &custom_preset) const { - return vector_contains(this->supported_custom_presets_, custom_preset); + for (const char *preset : this->supported_custom_presets_) { + if (strcmp(preset, custom_preset.c_str()) == 0) + return true; + } + return false; } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } @@ -239,8 +230,11 @@ class ClimateTraits { climate::ClimateFanModeMask supported_fan_modes_; climate::ClimateSwingModeMask supported_swing_modes_; climate::ClimatePresetMask supported_presets_; - std::vector supported_custom_fan_modes_; - std::vector supported_custom_presets_; + // Store const char* pointers to avoid std::string overhead + // Pointers must remain valid for traits lifetime (typically string literals in rodata, + // or pointers to strings with sufficient lifetime like member variables) + std::vector supported_custom_fan_modes_; + std::vector supported_custom_presets_; }; } // namespace climate diff --git a/esphome/components/midea/ac_adapter.cpp b/esphome/components/midea/ac_adapter.cpp index 2837713c35..dca4038f04 100644 --- a/esphome/components/midea/ac_adapter.cpp +++ b/esphome/components/midea/ac_adapter.cpp @@ -8,9 +8,9 @@ namespace midea { namespace ac { const char *const Constants::TAG = "midea"; -const std::string Constants::FREEZE_PROTECTION = "freeze protection"; -const std::string Constants::SILENT = "silent"; -const std::string Constants::TURBO = "turbo"; +const char *const Constants::FREEZE_PROTECTION = "freeze protection"; +const char *const Constants::SILENT = "silent"; +const char *const Constants::TURBO = "turbo"; ClimateMode Converters::to_climate_mode(MideaMode mode) { switch (mode) { @@ -108,7 +108,7 @@ bool Converters::is_custom_midea_fan_mode(MideaFanMode mode) { } } -const std::string &Converters::to_custom_climate_fan_mode(MideaFanMode mode) { +const char *Converters::to_custom_climate_fan_mode(MideaFanMode mode) { switch (mode) { case MideaFanMode::FAN_SILENT: return Constants::SILENT; @@ -151,7 +151,7 @@ ClimatePreset Converters::to_climate_preset(MideaPreset preset) { bool Converters::is_custom_midea_preset(MideaPreset preset) { return preset == MideaPreset::PRESET_FREEZE_PROTECTION; } -const std::string &Converters::to_custom_climate_preset(MideaPreset preset) { return Constants::FREEZE_PROTECTION; } +const char *Converters::to_custom_climate_preset(MideaPreset preset) { return Constants::FREEZE_PROTECTION; } MideaPreset Converters::to_midea_preset(const std::string &preset) { return MideaPreset::PRESET_FREEZE_PROTECTION; } @@ -169,7 +169,7 @@ void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea:: if (capabilities.supportEcoPreset()) traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_ECO); if (capabilities.supportFrostProtectionPreset()) - traits.add_supported_custom_preset(Constants::FREEZE_PROTECTION); + traits.set_supported_custom_presets({Constants::FREEZE_PROTECTION}); } } // namespace ac diff --git a/esphome/components/midea/ac_adapter.h b/esphome/components/midea/ac_adapter.h index c17894ae31..d52f421331 100644 --- a/esphome/components/midea/ac_adapter.h +++ b/esphome/components/midea/ac_adapter.h @@ -20,9 +20,9 @@ using MideaPreset = dudanov::midea::ac::Preset; class Constants { public: static const char *const TAG; - static const std::string FREEZE_PROTECTION; - static const std::string SILENT; - static const std::string TURBO; + static const char *const FREEZE_PROTECTION; + static const char *const SILENT; + static const char *const TURBO; }; class Converters { @@ -35,12 +35,12 @@ class Converters { static MideaPreset to_midea_preset(const std::string &preset); static bool is_custom_midea_preset(MideaPreset preset); static ClimatePreset to_climate_preset(MideaPreset preset); - static const std::string &to_custom_climate_preset(MideaPreset preset); + static const char *to_custom_climate_preset(MideaPreset preset); static MideaFanMode to_midea_fan_mode(ClimateFanMode fan_mode); static MideaFanMode to_midea_fan_mode(const std::string &fan_mode); static bool is_custom_midea_fan_mode(MideaFanMode fan_mode); static ClimateFanMode to_climate_fan_mode(MideaFanMode fan_mode); - static const std::string &to_custom_climate_fan_mode(MideaFanMode fan_mode); + static const char *to_custom_climate_fan_mode(MideaFanMode fan_mode); static void to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities); }; diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index 0ad26ebd51..97eacb936c 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -84,8 +84,10 @@ ClimateTraits AirConditioner::traits() { traits.set_supported_modes(this->supported_modes_); traits.set_supported_swing_modes(this->supported_swing_modes_); traits.set_supported_presets(this->supported_presets_); - traits.set_supported_custom_presets(this->supported_custom_presets_); - traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_); + if (!this->supported_custom_presets_.empty()) + traits.set_supported_custom_presets(this->supported_custom_presets_); + if (!this->supported_custom_fan_modes_.empty()) + traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_); /* + MINIMAL SET OF CAPABILITIES */ traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO); traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW); diff --git a/esphome/components/midea/air_conditioner.h b/esphome/components/midea/air_conditioner.h index 6c2401efe7..70833b8bcc 100644 --- a/esphome/components/midea/air_conditioner.h +++ b/esphome/components/midea/air_conditioner.h @@ -46,8 +46,8 @@ class AirConditioner : public ApplianceBase, void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } - void set_custom_presets(const std::vector &presets) { this->supported_custom_presets_ = presets; } - void set_custom_fan_modes(const std::vector &modes) { this->supported_custom_fan_modes_ = modes; } + void set_custom_presets(std::initializer_list presets) { this->supported_custom_presets_ = presets; } + void set_custom_fan_modes(std::initializer_list modes) { this->supported_custom_fan_modes_ = modes; } protected: void control(const ClimateCall &call) override; @@ -55,8 +55,8 @@ class AirConditioner : public ApplianceBase, ClimateModeMask supported_modes_{}; ClimateSwingModeMask supported_swing_modes_{}; ClimatePresetMask supported_presets_{}; - std::vector supported_custom_presets_{}; - std::vector supported_custom_fan_modes_{}; + std::vector supported_custom_presets_{}; + std::vector supported_custom_fan_modes_{}; Sensor *outdoor_sensor_{nullptr}; Sensor *humidity_sensor_{nullptr}; Sensor *power_sensor_{nullptr}; diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 394e92b9a7..3b756095a1 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1610,8 +1610,9 @@ class RepeatedTypeInfo(TypeInfo): # Other types need the actual value # Special handling for const char* elements if self._use_pointer and "const char" in self._container_no_template: + field_id_size = self.calculate_field_id_size() o += f" for (const char *it : {container_ref}) {{\n" - o += " size.add_length_force(1, strlen(it));\n" + o += f" size.add_length_force({field_id_size}, strlen(it));\n" else: auto_ref = "" if self._ti_is_bool else "&" o += f" for (const auto {auto_ref}it : {container_ref}) {{\n" From 789e435aacd75a55d519543d48a4854c76fa040d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 10:36:32 -0500 Subject: [PATCH 243/394] preen --- esphome/components/thermostat/climate.py | 20 ++++++++ .../climate_custom_fan_modes_and_presets.yaml | 44 ++++++++++++++++ .../integration/test_climate_custom_modes.py | 51 +++++++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml create mode 100644 tests/integration/test_climate_custom_modes.py diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index a928d208f3..cf592956b0 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_COOL_DEADBAND, CONF_COOL_MODE, CONF_COOL_OVERRUN, + CONF_CUSTOM_FAN_MODES, CONF_DEFAULT_MODE, CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_DEFAULT_TARGET_TEMPERATURE_LOW, @@ -658,6 +659,7 @@ CONFIG_SCHEMA = cv.All( } ), cv.Optional(CONF_PRESET): cv.ensure_list(PRESET_CONFIG_SCHEMA), + cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list(cv.string_strict), cv.Optional(CONF_ON_BOOT_RESTORE_FROM): validate_on_boot_restore_from, cv.Optional(CONF_PRESET_CHANGE): automation.validate_automation( single=True @@ -1008,3 +1010,21 @@ async def to_code(config): await automation.build_automation( var.get_preset_change_trigger(), [], config[CONF_PRESET_CHANGE] ) + + # Collect custom preset names from preset map (non-standard preset names only) + custom_preset_names = [ + preset_config[CONF_NAME] + for preset_config in config.get(CONF_PRESET, []) + if preset_config[CONF_NAME].upper() not in climate.CLIMATE_PRESETS + ] + if custom_preset_names: + cg.add(var.set_custom_presets(custom_preset_names)) + + # Collect custom fan modes (filter out standard enum fan modes) + custom_fan_modes = [ + mode + for mode in config.get(CONF_CUSTOM_FAN_MODES, []) + if mode.upper() not in climate.CLIMATE_FAN_MODES + ] + if custom_fan_modes: + cg.add(var.set_custom_fan_modes(custom_fan_modes)) diff --git a/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml b/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml new file mode 100644 index 0000000000..f006bb4352 --- /dev/null +++ b/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml @@ -0,0 +1,44 @@ +esphome: + name: climate-custom-modes-test +host: +api: +logger: + +sensor: + - platform: template + id: thermostat_sensor + lambda: "return 22.0;" + +climate: + - platform: thermostat + id: test_thermostat + name: Test Thermostat Custom Modes + sensor: thermostat_sensor + preset: + - name: Away + default_target_temperature_low: 16°C + default_target_temperature_high: 20°C + - name: Eco Plus + default_target_temperature_low: 18°C + default_target_temperature_high: 22°C + - name: Super Saver + default_target_temperature_low: 20°C + default_target_temperature_high: 24°C + - name: Vacation Mode + default_target_temperature_low: 15°C + default_target_temperature_high: 18°C + custom_fan_modes: + - "Turbo" + - "Silent" + - "Sleep Mode" + idle_action: + - logger.log: idle_action + cool_action: + - logger.log: cool_action + heat_action: + - logger.log: heat_action + min_cooling_off_time: 10s + min_cooling_run_time: 10s + min_heating_off_time: 10s + min_heating_run_time: 10s + min_idle_time: 10s diff --git a/tests/integration/test_climate_custom_modes.py b/tests/integration/test_climate_custom_modes.py new file mode 100644 index 0000000000..d88b682ccd --- /dev/null +++ b/tests/integration/test_climate_custom_modes.py @@ -0,0 +1,51 @@ +"""Integration test for climate custom fan modes and presets.""" + +from __future__ import annotations + +from aioesphomeapi import ClimateInfo, ClimatePreset +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_climate_custom_fan_modes_and_presets( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that custom fan modes and presets are properly exposed via API.""" + async with run_compiled(yaml_config), api_client_connected() as client: + # Get entities and services + entities, services = await client.list_entities_services() + climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] + assert len(climate_infos) == 1, "Expected exactly 1 climate entity" + + test_climate = climate_infos[0] + + # Verify custom fan modes are exposed + custom_fan_modes = test_climate.supported_custom_fan_modes + assert len(custom_fan_modes) == 3, ( + f"Expected 3 custom fan modes, got {len(custom_fan_modes)}" + ) + assert "Turbo" in custom_fan_modes, "Expected 'Turbo' in custom fan modes" + assert "Silent" in custom_fan_modes, "Expected 'Silent' in custom fan modes" + assert "Sleep Mode" in custom_fan_modes, ( + "Expected 'Sleep Mode' in custom fan modes" + ) + + # Verify enum presets are exposed (from preset: config map) + assert ClimatePreset.AWAY in test_climate.supported_presets, ( + "Expected AWAY in enum presets" + ) + + # Verify custom string presets are exposed (non-standard preset names from preset map) + custom_presets = test_climate.supported_custom_presets + assert len(custom_presets) == 3, ( + f"Expected 3 custom presets, got {len(custom_presets)}: {custom_presets}" + ) + assert "Eco Plus" in custom_presets, "Expected 'Eco Plus' in custom presets" + assert "Comfort" in custom_presets, "Expected 'Comfort' in custom presets" + assert "Vacation Mode" in custom_presets, ( + "Expected 'Vacation Mode' in custom presets" + ) From 9ed3f18893a1d5003b7a69df5e725b6f54530eaa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 10:39:30 -0500 Subject: [PATCH 244/394] preen --- .../thermostat/thermostat_climate.cpp | 18 ++++++++++++++++-- .../components/thermostat/thermostat_climate.h | 6 ++++++ tests/integration/test_climate_custom_modes.py | 4 +++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 18efe3984e..9b55a807c3 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -321,9 +321,15 @@ climate::ClimateTraits ThermostatClimate::traits() { for (auto &it : this->preset_config_) { traits.add_supported_preset(it.first); } - for (auto &it : this->custom_preset_config_) { - traits.add_supported_custom_preset(it.first); + + // Custom presets and fan modes are set directly from Python (includes all non-standard preset names from map) + if (!this->additional_custom_presets_.empty()) { + traits.set_supported_custom_presets(this->additional_custom_presets_); } + if (!this->additional_custom_fan_modes_.empty()) { + traits.set_supported_custom_fan_modes(this->additional_custom_fan_modes_); + } + return traits; } @@ -1613,6 +1619,14 @@ void ThermostatClimate::dump_config() { } } +void ThermostatClimate::set_custom_fan_modes(std::initializer_list custom_fan_modes) { + this->additional_custom_fan_modes_ = custom_fan_modes; +} + +void ThermostatClimate::set_custom_presets(std::initializer_list custom_presets) { + this->additional_custom_presets_ = custom_presets; +} + ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig() = default; ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig(float default_temperature) diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 42adab7751..a160c5e1a1 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -133,6 +133,8 @@ class ThermostatClimate : public climate::Climate, public Component { void set_preset_config(climate::ClimatePreset preset, const ThermostatClimateTargetTempConfig &config); void set_custom_preset_config(const std::string &name, const ThermostatClimateTargetTempConfig &config); + void set_custom_fan_modes(std::initializer_list custom_fan_modes); + void set_custom_presets(std::initializer_list custom_presets); Trigger<> *get_cool_action_trigger() const; Trigger<> *get_supplemental_cool_action_trigger() const; @@ -537,6 +539,10 @@ class ThermostatClimate : public climate::Climate, public Component { std::map preset_config_{}; /// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset") std::map custom_preset_config_{}; + /// Custom fan mode names (from Python codegen) + std::vector additional_custom_fan_modes_{}; + /// Custom preset names (from Python codegen) + std::vector additional_custom_presets_{}; }; } // namespace thermostat diff --git a/tests/integration/test_climate_custom_modes.py b/tests/integration/test_climate_custom_modes.py index d88b682ccd..4e0e8522ca 100644 --- a/tests/integration/test_climate_custom_modes.py +++ b/tests/integration/test_climate_custom_modes.py @@ -45,7 +45,9 @@ async def test_climate_custom_fan_modes_and_presets( f"Expected 3 custom presets, got {len(custom_presets)}: {custom_presets}" ) assert "Eco Plus" in custom_presets, "Expected 'Eco Plus' in custom presets" - assert "Comfort" in custom_presets, "Expected 'Comfort' in custom presets" + assert "Super Saver" in custom_presets, ( + "Expected 'Super Saver' in custom presets" + ) assert "Vacation Mode" in custom_presets, ( "Expected 'Vacation Mode' in custom presets" ) From fa424514db5fab0fe618df4faeb917b27129513b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 10:44:23 -0500 Subject: [PATCH 245/394] remove testing --- esphome/components/thermostat/climate.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index cf592956b0..a883c47582 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -9,7 +9,6 @@ from esphome.const import ( CONF_COOL_DEADBAND, CONF_COOL_MODE, CONF_COOL_OVERRUN, - CONF_CUSTOM_FAN_MODES, CONF_DEFAULT_MODE, CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_DEFAULT_TARGET_TEMPERATURE_LOW, @@ -659,7 +658,6 @@ CONFIG_SCHEMA = cv.All( } ), cv.Optional(CONF_PRESET): cv.ensure_list(PRESET_CONFIG_SCHEMA), - cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list(cv.string_strict), cv.Optional(CONF_ON_BOOT_RESTORE_FROM): validate_on_boot_restore_from, cv.Optional(CONF_PRESET_CHANGE): automation.validate_automation( single=True @@ -1019,12 +1017,3 @@ async def to_code(config): ] if custom_preset_names: cg.add(var.set_custom_presets(custom_preset_names)) - - # Collect custom fan modes (filter out standard enum fan modes) - custom_fan_modes = [ - mode - for mode in config.get(CONF_CUSTOM_FAN_MODES, []) - if mode.upper() not in climate.CLIMATE_FAN_MODES - ] - if custom_fan_modes: - cg.add(var.set_custom_fan_modes(custom_fan_modes)) From 10d6281edc6d5d534d596b2c61c58e4cffa9926f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 10:44:36 -0500 Subject: [PATCH 246/394] remove testing --- esphome/components/thermostat/thermostat_climate.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index a160c5e1a1..d4ee178f4b 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -133,7 +133,6 @@ class ThermostatClimate : public climate::Climate, public Component { void set_preset_config(climate::ClimatePreset preset, const ThermostatClimateTargetTempConfig &config); void set_custom_preset_config(const std::string &name, const ThermostatClimateTargetTempConfig &config); - void set_custom_fan_modes(std::initializer_list custom_fan_modes); void set_custom_presets(std::initializer_list custom_presets); Trigger<> *get_cool_action_trigger() const; @@ -539,8 +538,6 @@ class ThermostatClimate : public climate::Climate, public Component { std::map preset_config_{}; /// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset") std::map custom_preset_config_{}; - /// Custom fan mode names (from Python codegen) - std::vector additional_custom_fan_modes_{}; /// Custom preset names (from Python codegen) std::vector additional_custom_presets_{}; }; From ccfdd0cf0634c280935c030547f2f63419fb47ef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 10:44:49 -0500 Subject: [PATCH 247/394] remove testing --- esphome/components/thermostat/thermostat_climate.cpp | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 9b55a807c3..752204004a 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -322,13 +322,10 @@ climate::ClimateTraits ThermostatClimate::traits() { traits.add_supported_preset(it.first); } - // Custom presets and fan modes are set directly from Python (includes all non-standard preset names from map) + // Custom presets are set directly from Python (includes all non-standard preset names from map) if (!this->additional_custom_presets_.empty()) { traits.set_supported_custom_presets(this->additional_custom_presets_); } - if (!this->additional_custom_fan_modes_.empty()) { - traits.set_supported_custom_fan_modes(this->additional_custom_fan_modes_); - } return traits; } @@ -1619,10 +1616,6 @@ void ThermostatClimate::dump_config() { } } -void ThermostatClimate::set_custom_fan_modes(std::initializer_list custom_fan_modes) { - this->additional_custom_fan_modes_ = custom_fan_modes; -} - void ThermostatClimate::set_custom_presets(std::initializer_list custom_presets) { this->additional_custom_presets_ = custom_presets; } From bf1514e6722b774f353716cdbaaeae34596fc632 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 10:46:32 -0500 Subject: [PATCH 248/394] preen --- esphome/components/thermostat/climate.py | 9 --------- .../components/thermostat/thermostat_climate.cpp | 15 ++++++++------- .../components/thermostat/thermostat_climate.h | 3 --- .../climate_custom_fan_modes_and_presets.yaml | 4 ---- tests/integration/test_climate_custom_modes.py | 15 ++------------- 5 files changed, 10 insertions(+), 36 deletions(-) diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index a883c47582..a928d208f3 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -1008,12 +1008,3 @@ async def to_code(config): await automation.build_automation( var.get_preset_change_trigger(), [], config[CONF_PRESET_CHANGE] ) - - # Collect custom preset names from preset map (non-standard preset names only) - custom_preset_names = [ - preset_config[CONF_NAME] - for preset_config in config.get(CONF_PRESET, []) - if preset_config[CONF_NAME].upper() not in climate.CLIMATE_PRESETS - ] - if custom_preset_names: - cg.add(var.set_custom_presets(custom_preset_names)) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 752204004a..6842bd4be8 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -322,9 +322,14 @@ climate::ClimateTraits ThermostatClimate::traits() { traits.add_supported_preset(it.first); } - // Custom presets are set directly from Python (includes all non-standard preset names from map) - if (!this->additional_custom_presets_.empty()) { - traits.set_supported_custom_presets(this->additional_custom_presets_); + // Extract custom preset names from the custom_preset_config_ map + if (!this->custom_preset_config_.empty()) { + std::vector custom_preset_names; + custom_preset_names.reserve(this->custom_preset_config_.size()); + for (const auto &it : this->custom_preset_config_) { + custom_preset_names.push_back(it.first.c_str()); + } + traits.set_supported_custom_presets(custom_preset_names); } return traits; @@ -1616,10 +1621,6 @@ void ThermostatClimate::dump_config() { } } -void ThermostatClimate::set_custom_presets(std::initializer_list custom_presets) { - this->additional_custom_presets_ = custom_presets; -} - ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig() = default; ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig(float default_temperature) diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index d4ee178f4b..42adab7751 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -133,7 +133,6 @@ class ThermostatClimate : public climate::Climate, public Component { void set_preset_config(climate::ClimatePreset preset, const ThermostatClimateTargetTempConfig &config); void set_custom_preset_config(const std::string &name, const ThermostatClimateTargetTempConfig &config); - void set_custom_presets(std::initializer_list custom_presets); Trigger<> *get_cool_action_trigger() const; Trigger<> *get_supplemental_cool_action_trigger() const; @@ -538,8 +537,6 @@ class ThermostatClimate : public climate::Climate, public Component { std::map preset_config_{}; /// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset") std::map custom_preset_config_{}; - /// Custom preset names (from Python codegen) - std::vector additional_custom_presets_{}; }; } // namespace thermostat diff --git a/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml b/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml index f006bb4352..bf4ef9eafd 100644 --- a/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml +++ b/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml @@ -27,10 +27,6 @@ climate: - name: Vacation Mode default_target_temperature_low: 15°C default_target_temperature_high: 18°C - custom_fan_modes: - - "Turbo" - - "Silent" - - "Sleep Mode" idle_action: - logger.log: idle_action cool_action: diff --git a/tests/integration/test_climate_custom_modes.py b/tests/integration/test_climate_custom_modes.py index 4e0e8522ca..ce34959d88 100644 --- a/tests/integration/test_climate_custom_modes.py +++ b/tests/integration/test_climate_custom_modes.py @@ -1,4 +1,4 @@ -"""Integration test for climate custom fan modes and presets.""" +"""Integration test for climate custom presets.""" from __future__ import annotations @@ -14,7 +14,7 @@ async def test_climate_custom_fan_modes_and_presets( run_compiled: RunCompiledFunction, api_client_connected: APIClientConnectedFactory, ) -> None: - """Test that custom fan modes and presets are properly exposed via API.""" + """Test that custom presets are properly exposed via API.""" async with run_compiled(yaml_config), api_client_connected() as client: # Get entities and services entities, services = await client.list_entities_services() @@ -23,17 +23,6 @@ async def test_climate_custom_fan_modes_and_presets( test_climate = climate_infos[0] - # Verify custom fan modes are exposed - custom_fan_modes = test_climate.supported_custom_fan_modes - assert len(custom_fan_modes) == 3, ( - f"Expected 3 custom fan modes, got {len(custom_fan_modes)}" - ) - assert "Turbo" in custom_fan_modes, "Expected 'Turbo' in custom fan modes" - assert "Silent" in custom_fan_modes, "Expected 'Silent' in custom fan modes" - assert "Sleep Mode" in custom_fan_modes, ( - "Expected 'Sleep Mode' in custom fan modes" - ) - # Verify enum presets are exposed (from preset: config map) assert ClimatePreset.AWAY in test_climate.supported_presets, ( "Expected AWAY in enum presets" From 8f9f00df83e3e077beb5c9e484a54f627dd014ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 10:55:06 -0500 Subject: [PATCH 249/394] preen --- esphome/components/climate/climate_traits.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index b9789d9ccb..f0e0dbe02b 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -131,6 +131,9 @@ class ClimateTraits { void set_supported_custom_fan_modes(const std::vector &modes) { this->supported_custom_fan_modes_ = modes; } + template void set_supported_custom_fan_modes(const char *const (&modes)[N]) { + this->supported_custom_fan_modes_.assign(modes, modes + N); + } const std::vector &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { for (const char *mode : this->supported_custom_fan_modes_) { @@ -152,6 +155,9 @@ class ClimateTraits { void set_supported_custom_presets(const std::vector &presets) { this->supported_custom_presets_ = presets; } + template void set_supported_custom_presets(const char *const (&presets)[N]) { + this->supported_custom_presets_.assign(presets, presets + N); + } const std::vector &get_supported_custom_presets() const { return this->supported_custom_presets_; } bool supports_custom_preset(const std::string &custom_preset) const { for (const char *preset : this->supported_custom_presets_) { From 721252d2194b86dedc51819ae4f8de64c9a6c0c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 10:56:19 -0500 Subject: [PATCH 250/394] preen --- esphome/components/bedjet/climate/bedjet_climate.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/bedjet/climate/bedjet_climate.cpp b/esphome/components/bedjet/climate/bedjet_climate.cpp index f22d312b5a..65fa092e8e 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.cpp +++ b/esphome/components/bedjet/climate/bedjet_climate.cpp @@ -48,7 +48,7 @@ void BedJetClimate::dump_config() { ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode))); } for (const auto &mode : traits.get_supported_custom_fan_modes()) { - ESP_LOGCONFIG(TAG, " - %s (c)", mode.c_str()); + ESP_LOGCONFIG(TAG, " - %s (c)", mode); } ESP_LOGCONFIG(TAG, " Supported presets:"); @@ -56,7 +56,7 @@ void BedJetClimate::dump_config() { ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset))); } for (const auto &preset : traits.get_supported_custom_presets()) { - ESP_LOGCONFIG(TAG, " - %s (c)", preset.c_str()); + ESP_LOGCONFIG(TAG, " - %s (c)", preset); } } From 0db55ef2dd7a07aabb994764fbf54d1b673f79b6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 12:14:53 -0500 Subject: [PATCH 251/394] select by index --- esphome/components/api/api_connection.cpp | 2 +- .../logger/select/logger_level_select.cpp | 6 +- esphome/components/select/select.cpp | 39 ++++++++--- esphome/components/select/select.h | 20 +++++- esphome/components/select/select_call.cpp | 69 +++++++++---------- esphome/components/select/select_call.h | 3 +- .../template/select/template_select.cpp | 2 +- .../components/tuya/select/tuya_select.cpp | 3 +- 8 files changed, 86 insertions(+), 58 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 382c4acc16..69c7efea32 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -877,7 +877,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection bool is_single) { auto *select = static_cast(entity); SelectStateResponse resp; - resp.set_state(StringRef(select->state)); + resp.set_state(StringRef(select->current_option())); resp.missing_state = !select->has_state(); return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } diff --git a/esphome/components/logger/select/logger_level_select.cpp b/esphome/components/logger/select/logger_level_select.cpp index 6d60a3ae47..5537df570c 100644 --- a/esphome/components/logger/select/logger_level_select.cpp +++ b/esphome/components/logger/select/logger_level_select.cpp @@ -3,10 +3,10 @@ namespace esphome::logger { void LoggerLevelSelect::publish_state(int level) { - const auto &option = this->at(level_to_index(level)); - if (!option) + auto index = level_to_index(level); + if (!this->has_index(index)) return; - Select::publish_state(option.value()); + Select::publish_state(index); } void LoggerLevelSelect::setup() { diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 5e30be3c13..b52504ea28 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -7,24 +7,39 @@ namespace select { static const char *const TAG = "select"; -void Select::publish_state(const std::string &state) { +void Select::publish_state(const std::string &state) { this->publish_state(state.c_str()); } + +void Select::publish_state(const char *state) { auto index = this->index_of(state); - const auto *name = this->get_name().c_str(); if (index.has_value()) { - this->set_has_state(true); - this->state = state; - ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", name, state.c_str(), index.value()); - this->state_callback_.call(state, index.value()); + this->publish_state(index.value()); } else { - ESP_LOGE(TAG, "'%s': invalid state for publish_state(): %s", name, state.c_str()); + ESP_LOGE(TAG, "'%s': invalid state for publish_state(): %s", this->get_name().c_str(), state); } } +void Select::publish_state(size_t index) { + if (!this->has_index(index)) { + ESP_LOGE(TAG, "'%s': invalid index for publish_state(): %zu", this->get_name().c_str(), index); + return; + } + const char *option = this->option_at(index); + this->set_has_state(true); + this->active_index_ = index; + ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", this->get_name().c_str(), option, index); + // Callback signature requires std::string, create temporary for compatibility + this->state_callback_.call(std::string(option), index); +} + +const char *Select::current_option() const { return this->option_at(this->active_index_); } + void Select::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } -bool Select::has_option(const std::string &option) const { return this->index_of(option).has_value(); } +bool Select::has_option(const std::string &option) const { return this->index_of(option.c_str()).has_value(); } + +bool Select::has_option(const char *option) const { return this->index_of(option).has_value(); } bool Select::has_index(size_t index) const { return index < this->size(); } @@ -33,10 +48,12 @@ size_t Select::size() const { return options.size(); } -optional Select::index_of(const std::string &option) const { +optional Select::index_of(const std::string &option) const { return this->index_of(option.c_str()); } + +optional Select::index_of(const char *option) const { const auto &options = traits.get_options(); for (size_t i = 0; i < options.size(); i++) { - if (strcmp(options[i], option.c_str()) == 0) { + if (strcmp(options[i], option) == 0) { return i; } } @@ -45,7 +62,7 @@ optional Select::index_of(const std::string &option) const { optional Select::active_index() const { if (this->has_state()) { - return this->index_of(this->state); + return this->active_index_; } else { return {}; } diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index eabb39898b..81c8f68362 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -30,16 +30,21 @@ namespace select { */ class Select : public EntityBase { public: - std::string state; SelectTraits traits; void publish_state(const std::string &state); + void publish_state(const char *state); + void publish_state(size_t index); + + /// Return the currently selected option (as const char* from flash). + const char *current_option() const; /// Instantiate a SelectCall object to modify this select component's state. SelectCall make_call() { return SelectCall(this); } /// Return whether this select component contains the provided option. bool has_option(const std::string &option) const; + bool has_option(const char *option) const; /// Return whether this select component contains the provided index offset. bool has_index(size_t index) const; @@ -49,6 +54,7 @@ class Select : public EntityBase { /// Find the (optional) index offset of the provided option value. optional index_of(const std::string &option) const; + optional index_of(const char *option) const; /// Return the (optional) index offset of the currently active option. optional active_index() const; @@ -64,6 +70,18 @@ class Select : public EntityBase { protected: friend class SelectCall; + size_t active_index_{0}; + + /** Set the value of the select by index, this is an optional virtual method. + * + * This method is called by the SelectCall when the index is already known. + * Default implementation converts to string and calls control(). + * Override this to work directly with indices and avoid string conversions. + * + * @param index The index as validated by the SelectCall. + */ + virtual void control(size_t index) { this->control(this->option_at(index)); } + /** Set the value of the select, this is a virtual method that each select integration must implement. * * This method is called by the SelectCall. diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index dd398b4052..144090e21f 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -11,6 +11,8 @@ SelectCall &SelectCall::set_option(const std::string &option) { return with_operation(SELECT_OP_SET).with_option(option); } +SelectCall &SelectCall::set_option(const char *option) { return with_operation(SELECT_OP_SET).with_option(option); } + SelectCall &SelectCall::set_index(size_t index) { return with_operation(SELECT_OP_SET_INDEX).with_index(index); } SelectCall &SelectCall::select_next(bool cycle) { return with_operation(SELECT_OP_NEXT).with_cycle(cycle); } @@ -31,8 +33,11 @@ SelectCall &SelectCall::with_cycle(bool cycle) { return *this; } -SelectCall &SelectCall::with_option(const std::string &option) { - this->option_ = option; +SelectCall &SelectCall::with_option(const std::string &option) { return this->with_option(option.c_str()); } + +SelectCall &SelectCall::with_option(const char *option) { + // Find the option index - this validates the option exists + this->index_ = this->parent_->index_of(option); return *this; } @@ -56,64 +61,52 @@ void SelectCall::perform() { return; } - std::string target_value; + size_t target_index; - if (this->operation_ == SELECT_OP_SET) { - ESP_LOGD(TAG, "'%s' - Setting", name); - if (!this->option_.has_value()) { - ESP_LOGW(TAG, "'%s' - No option value set for SelectCall", name); - return; + if (this->operation_ == SELECT_OP_SET || this->operation_ == SELECT_OP_SET_INDEX) { + if (this->operation_ == SELECT_OP_SET) { + ESP_LOGD(TAG, "'%s' - Setting", name); } - target_value = this->option_.value(); - } else if (this->operation_ == SELECT_OP_SET_INDEX) { if (!this->index_.has_value()) { - ESP_LOGW(TAG, "'%s' - No index value set for SelectCall", name); + ESP_LOGW(TAG, "'%s' - No option value set for SelectCall", name); return; } if (this->index_.value() >= options.size()) { ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", name, this->index_.value()); return; } - target_value = options[this->index_.value()]; + target_index = this->index_.value(); } else if (this->operation_ == SELECT_OP_FIRST) { - target_value = options.front(); + target_index = 0; } else if (this->operation_ == SELECT_OP_LAST) { - target_value = options.back(); + target_index = options.size() - 1; } else if (this->operation_ == SELECT_OP_NEXT || this->operation_ == SELECT_OP_PREVIOUS) { auto cycle = this->cycle_; ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, this->operation_ == SELECT_OP_NEXT ? "next" : "previous", cycle ? "" : "out"); if (!parent->has_state()) { - target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back(); + target_index = this->operation_ == SELECT_OP_NEXT ? 0 : options.size() - 1; } else { - auto index = parent->index_of(parent->state); - if (index.has_value()) { - auto size = options.size(); - if (cycle) { - auto use_index = (size + index.value() + (this->operation_ == SELECT_OP_NEXT ? +1 : -1)) % size; - target_value = options[use_index]; - } else { - if (this->operation_ == SELECT_OP_PREVIOUS && index.value() > 0) { - target_value = options[index.value() - 1]; - } else if (this->operation_ == SELECT_OP_NEXT && index.value() < options.size() - 1) { - target_value = options[index.value() + 1]; - } else { - return; - } - } + // Use cached active_index_ instead of index_of() lookup + auto index = parent->active_index_; + auto size = options.size(); + if (cycle) { + target_index = (size + index + (this->operation_ == SELECT_OP_NEXT ? +1 : -1)) % size; } else { - target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back(); + if (this->operation_ == SELECT_OP_PREVIOUS && index > 0) { + target_index = index - 1; + } else if (this->operation_ == SELECT_OP_NEXT && index < options.size() - 1) { + target_index = index + 1; + } else { + return; + } } } } - if (!parent->has_option(target_value)) { - ESP_LOGW(TAG, "'%s' - Option %s is not a valid option", name, target_value.c_str()); - return; - } - - ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, target_value.c_str()); - parent->control(target_value); + // All operations use indices, call control() by index to avoid string conversion + ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, options[target_index]); + parent->control(target_index); } } // namespace select diff --git a/esphome/components/select/select_call.h b/esphome/components/select/select_call.h index efc9a982ec..a0c63a0e39 100644 --- a/esphome/components/select/select_call.h +++ b/esphome/components/select/select_call.h @@ -23,6 +23,7 @@ class SelectCall { void perform(); SelectCall &set_option(const std::string &option); + SelectCall &set_option(const char *option); SelectCall &set_index(size_t index); SelectCall &select_next(bool cycle); @@ -33,11 +34,11 @@ class SelectCall { SelectCall &with_operation(SelectOperation operation); SelectCall &with_cycle(bool cycle); SelectCall &with_option(const std::string &option); + SelectCall &with_option(const char *option); SelectCall &with_index(size_t index); protected: Select *const parent_; - optional option_; optional index_; SelectOperation operation_{SELECT_OP_NONE}; bool cycle_; diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 3ea34c3c7c..03ef1d482b 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -24,7 +24,7 @@ void TemplateSelect::setup() { ESP_LOGD(TAG, "State from initial: %s", this->option_at(index)); } - this->publish_state(this->at(index).value()); + this->publish_state(index); } void TemplateSelect::update() { diff --git a/esphome/components/tuya/select/tuya_select.cpp b/esphome/components/tuya/select/tuya_select.cpp index 7c1cd09d06..07e3ce44ee 100644 --- a/esphome/components/tuya/select/tuya_select.cpp +++ b/esphome/components/tuya/select/tuya_select.cpp @@ -17,8 +17,7 @@ void TuyaSelect::setup() { return; } size_t mapping_idx = std::distance(mappings.cbegin(), it); - auto value = this->at(mapping_idx); - this->publish_state(value.value()); + this->publish_state(mapping_idx); }); } From 18783ff20b48d40887e829a7028ce11147c48e85 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 12:26:47 -0500 Subject: [PATCH 252/394] preen --- esphome/components/copy/select/copy_select.cpp | 4 ++-- esphome/components/display_menu_base/menu_item.cpp | 2 +- esphome/components/ld2410/ld2410.cpp | 6 +++--- esphome/components/ld2412/ld2412.cpp | 6 +++--- esphome/components/ld2450/ld2450.cpp | 6 +++--- esphome/components/mqtt/mqtt_select.cpp | 5 +++-- esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp | 10 +++++----- esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp | 6 +++--- esphome/components/select/select.cpp | 4 ++-- 9 files changed, 25 insertions(+), 24 deletions(-) diff --git a/esphome/components/copy/select/copy_select.cpp b/esphome/components/copy/select/copy_select.cpp index bdcbd0b42c..fde7e3d92d 100644 --- a/esphome/components/copy/select/copy_select.cpp +++ b/esphome/components/copy/select/copy_select.cpp @@ -7,12 +7,12 @@ namespace copy { static const char *const TAG = "copy.select"; void CopySelect::setup() { - source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); }); + source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(index); }); traits.set_options(source_->traits.get_options()); if (source_->has_state()) - this->publish_state(source_->state); + this->publish_state(source_->current_option()); } void CopySelect::dump_config() { LOG_SELECT("", "Copy Select", this); } diff --git a/esphome/components/display_menu_base/menu_item.cpp b/esphome/components/display_menu_base/menu_item.cpp index 2c7f34c493..8224adf3fe 100644 --- a/esphome/components/display_menu_base/menu_item.cpp +++ b/esphome/components/display_menu_base/menu_item.cpp @@ -42,7 +42,7 @@ std::string MenuItemSelect::get_value_text() const { result = this->value_getter_.value()(this); } else { if (this->select_var_ != nullptr) { - result = this->select_var_->state; + result = this->select_var_->current_option(); } } diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index 5c3af54ad8..dd796701f8 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -441,7 +441,7 @@ bool LD2410Component::handle_ack_data_() { ESP_LOGV(TAG, "Baud rate change"); #ifdef USE_SELECT if (this->baud_rate_select_ != nullptr) { - ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str()); + ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option()); } #endif break; @@ -759,10 +759,10 @@ void LD2410Component::set_light_out_control() { #endif #ifdef USE_SELECT if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) { - this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->state); + this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option()); } if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->has_state()) { - this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->state); + this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option()); } #endif this->set_config_mode_(true); diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index 63af69ce0d..84af40fafb 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -485,7 +485,7 @@ bool LD2412Component::handle_ack_data_() { ESP_LOGV(TAG, "Baud rate change"); #ifdef USE_SELECT if (this->baud_rate_select_ != nullptr) { - ESP_LOGW(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str()); + ESP_LOGW(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option()); } #endif break; @@ -783,7 +783,7 @@ void LD2412Component::set_basic_config() { 1, TOTAL_GATES, DEFAULT_PRESENCE_TIMEOUT, 0, #endif #ifdef USE_SELECT - find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->state), + find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option()), #else 0x01, // Default value if not using select #endif @@ -837,7 +837,7 @@ void LD2412Component::set_light_out_control() { #endif #ifdef USE_SELECT if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) { - this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->state); + this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option()); } #endif uint8_t value[2] = {this->light_function_, this->light_threshold_}; diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index f30752e5a2..3a20baaf66 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -380,7 +380,7 @@ void LD2450Component::read_all_info() { this->set_config_mode_(false); #ifdef USE_SELECT const auto baud_rate = std::to_string(this->parent_->get_baud_rate()); - if (this->baud_rate_select_ != nullptr && this->baud_rate_select_->state != baud_rate) { + if (this->baud_rate_select_ != nullptr && strcmp(this->baud_rate_select_->current_option(), baud_rate.c_str()) != 0) { this->baud_rate_select_->publish_state(baud_rate); } this->publish_zone_type(); @@ -635,7 +635,7 @@ bool LD2450Component::handle_ack_data_() { ESP_LOGV(TAG, "Baud rate change"); #ifdef USE_SELECT if (this->baud_rate_select_ != nullptr) { - ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str()); + ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option()); } #endif break; @@ -716,7 +716,7 @@ bool LD2450Component::handle_ack_data_() { this->publish_zone_type(); #ifdef USE_SELECT if (this->zone_type_select_ != nullptr) { - ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->state.c_str()); + ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->current_option()); } #endif if (this->buffer_data_[10] == 0x00) { diff --git a/esphome/components/mqtt/mqtt_select.cpp b/esphome/components/mqtt/mqtt_select.cpp index b851348306..e1660b07ea 100644 --- a/esphome/components/mqtt/mqtt_select.cpp +++ b/esphome/components/mqtt/mqtt_select.cpp @@ -21,7 +21,8 @@ void MQTTSelectComponent::setup() { call.set_option(state); call.perform(); }); - this->select_->add_on_state_callback([this](const std::string &state, size_t index) { this->publish_state(state); }); + this->select_->add_on_state_callback( + [this](const std::string &state, size_t index) { this->publish_state(this->select_->option_at(index)); }); } void MQTTSelectComponent::dump_config() { @@ -44,7 +45,7 @@ void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon } bool MQTTSelectComponent::send_initial_state() { if (this->select_->has_state()) { - return this->publish_state(this->select_->state); + return this->publish_state(this->select_->current_option()); } else { return true; } diff --git a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp index 76523ce5c0..4c0416d727 100644 --- a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp +++ b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp @@ -435,12 +435,12 @@ void MR24HPC1Component::r24_frame_parse_open_underlying_information_(uint8_t *da } else if ((this->existence_boundary_select_ != nullptr) && ((data[FRAME_COMMAND_WORD_INDEX] == 0x0a) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8a))) { if (this->existence_boundary_select_->has_index(data[FRAME_DATA_INDEX] - 1)) { - this->existence_boundary_select_->publish_state(S_BOUNDARY_STR[data[FRAME_DATA_INDEX] - 1]); + this->existence_boundary_select_->publish_state(data[FRAME_DATA_INDEX] - 1); } } else if ((this->motion_boundary_select_ != nullptr) && ((data[FRAME_COMMAND_WORD_INDEX] == 0x0b) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8b))) { if (this->motion_boundary_select_->has_index(data[FRAME_DATA_INDEX] - 1)) { - this->motion_boundary_select_->publish_state(S_BOUNDARY_STR[data[FRAME_DATA_INDEX] - 1]); + this->motion_boundary_select_->publish_state(data[FRAME_DATA_INDEX] - 1); } } else if ((this->motion_trigger_number_ != nullptr) && ((data[FRAME_COMMAND_WORD_INDEX] == 0x0c) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8c))) { @@ -515,7 +515,7 @@ void MR24HPC1Component::r24_frame_parse_work_status_(uint8_t *data) { ESP_LOGD(TAG, "Reply: get radar init status 0x%02X", data[FRAME_DATA_INDEX]); } else if (data[FRAME_COMMAND_WORD_INDEX] == 0x07) { if ((this->scene_mode_select_ != nullptr) && (this->scene_mode_select_->has_index(data[FRAME_DATA_INDEX]))) { - this->scene_mode_select_->publish_state(S_SCENE_STR[data[FRAME_DATA_INDEX]]); + this->scene_mode_select_->publish_state(data[FRAME_DATA_INDEX]); } else { ESP_LOGD(TAG, "Select has index offset %d Error", data[FRAME_DATA_INDEX]); } @@ -538,7 +538,7 @@ void MR24HPC1Component::r24_frame_parse_work_status_(uint8_t *data) { ESP_LOGD(TAG, "Reply: get radar init status 0x%02X", data[FRAME_DATA_INDEX]); } else if (data[FRAME_COMMAND_WORD_INDEX] == 0x87) { if ((this->scene_mode_select_ != nullptr) && (this->scene_mode_select_->has_index(data[FRAME_DATA_INDEX]))) { - this->scene_mode_select_->publish_state(S_SCENE_STR[data[FRAME_DATA_INDEX]]); + this->scene_mode_select_->publish_state(data[FRAME_DATA_INDEX]); } else { ESP_LOGD(TAG, "Select has index offset %d Error", data[FRAME_DATA_INDEX]); } @@ -581,7 +581,7 @@ void MR24HPC1Component::r24_frame_parse_human_information_(uint8_t *data) { ((data[FRAME_COMMAND_WORD_INDEX] == 0x0A) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8A))) { // none:0x00 1s:0x01 30s:0x02 1min:0x03 2min:0x04 5min:0x05 10min:0x06 30min:0x07 1hour:0x08 if (data[FRAME_DATA_INDEX] < 9) { - this->unman_time_select_->publish_state(S_UNMANNED_TIME_STR[data[FRAME_DATA_INDEX]]); + this->unman_time_select_->publish_state(data[FRAME_DATA_INDEX]); } } else if ((this->keep_away_text_sensor_ != nullptr) && ((data[FRAME_COMMAND_WORD_INDEX] == 0x0B) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8B))) { diff --git a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp index dea7976578..7f8bd6a43c 100644 --- a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp +++ b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp @@ -292,7 +292,7 @@ void MR60FDA2Component::process_frame_() { install_height_float = bit_cast(current_install_height_int); uint32_t select_index = find_nearest_index(install_height_float, INSTALL_HEIGHT, 7); - this->install_height_select_->publish_state(this->install_height_select_->at(select_index).value()); + this->install_height_select_->publish_state(select_index); } if (this->height_threshold_select_ != nullptr) { @@ -301,7 +301,7 @@ void MR60FDA2Component::process_frame_() { height_threshold_float = bit_cast(current_height_threshold_int); size_t select_index = find_nearest_index(height_threshold_float, HEIGHT_THRESHOLD, 7); - this->height_threshold_select_->publish_state(this->height_threshold_select_->at(select_index).value()); + this->height_threshold_select_->publish_state(select_index); } if (this->sensitivity_select_ != nullptr) { @@ -309,7 +309,7 @@ void MR60FDA2Component::process_frame_() { encode_uint32(current_data_buf_[11], current_data_buf_[10], current_data_buf_[9], current_data_buf_[8]); uint32_t select_index = find_nearest_index(current_sensitivity, SENSITIVITY, 3); - this->sensitivity_select_->publish_state(this->sensitivity_select_->at(select_index).value()); + this->sensitivity_select_->publish_state(select_index); } ESP_LOGD(TAG, "Mounting height: %.2f, Height threshold: %.2f, Sensitivity: %" PRIu32, install_height_float, diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index b52504ea28..63192623d3 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -14,13 +14,13 @@ void Select::publish_state(const char *state) { if (index.has_value()) { this->publish_state(index.value()); } else { - ESP_LOGE(TAG, "'%s': invalid state for publish_state(): %s", this->get_name().c_str(), state); + ESP_LOGE(TAG, "'%s': Invalid option %s", this->get_name().c_str(), state); } } void Select::publish_state(size_t index) { if (!this->has_index(index)) { - ESP_LOGE(TAG, "'%s': invalid index for publish_state(): %zu", this->get_name().c_str(), index); + ESP_LOGE(TAG, "'%s': Invalid index %zu", this->get_name().c_str(), index); return; } const char *option = this->option_at(index); From df014f0217ae7ab477af310302a3310fd5e6ba08 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 12:28:19 -0500 Subject: [PATCH 253/394] preen --- esphome/components/web_server/web_server.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 1d08ef5a35..75ecc994c3 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1185,7 +1185,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->select_json(obj, obj->state, detail); + std::string data = this->select_json(obj, obj->current_option(), detail); request->send(200, "application/json", data.c_str()); return; } @@ -1205,10 +1205,12 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM request->send(404); } std::string WebServer::select_state_json_generator(WebServer *web_server, void *source) { - return web_server->select_json((select::Select *) (source), ((select::Select *) (source))->state, DETAIL_STATE); + return web_server->select_json((select::Select *) (source), ((select::Select *) (source))->current_option(), + DETAIL_STATE); } std::string WebServer::select_all_json_generator(WebServer *web_server, void *source) { - return web_server->select_json((select::Select *) (source), ((select::Select *) (source))->state, DETAIL_ALL); + return web_server->select_json((select::Select *) (source), ((select::Select *) (source))->current_option(), + DETAIL_ALL); } std::string WebServer::select_json(select::Select *obj, const std::string &value, JsonDetail start_config) { json::JsonBuilder builder; From 1c0a5a9765849ea5daf51071c7166c7ba1ac4b38 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 12:32:37 -0500 Subject: [PATCH 254/394] preen --- esphome/components/ld2410/ld2410.cpp | 8 ++++---- esphome/components/ld2412/ld2412.cpp | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index dd796701f8..ecb2920007 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -121,9 +121,9 @@ constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = { }; // Helper functions for lookups -template uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { +template uint8_t find_uint8(const StringToUint8 (&arr)[N], const char *str) { for (const auto &entry : arr) { - if (str == entry.str) + if (strcmp(str, entry.str) == 0) return entry.value; } return 0xFF; // Not found @@ -628,14 +628,14 @@ void LD2410Component::set_bluetooth(bool enable) { void LD2410Component::set_distance_resolution(const std::string &state) { this->set_config_mode_(true); - const uint8_t cmd_value[2] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00}; + const uint8_t cmd_value[2] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state.c_str()), 0x00}; this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value)); this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); } void LD2410Component::set_baud_rate(const std::string &state) { this->set_config_mode_(true); - const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; + const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state.c_str()), 0x00}; this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value)); this->set_timeout(200, [this]() { this->restart_(); }); } diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index 84af40fafb..b9bfd6ae49 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -132,9 +132,9 @@ constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = { }; // Helper functions for lookups -template uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { +template uint8_t find_uint8(const StringToUint8 (&arr)[N], const char *str) { for (const auto &entry : arr) { - if (str == entry.str) { + if (strcmp(str, entry.str) == 0) { return entry.value; } } @@ -701,14 +701,14 @@ void LD2412Component::set_bluetooth(bool enable) { void LD2412Component::set_distance_resolution(const std::string &state) { this->set_config_mode_(true); - const uint8_t cmd_value[6] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t cmd_value[6] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state.c_str()), 0x00, 0x00, 0x00, 0x00, 0x00}; this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value)); this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); } void LD2412Component::set_baud_rate(const std::string &state) { this->set_config_mode_(true); - const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; + const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state.c_str()), 0x00}; this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value)); this->set_timeout(200, [this]() { this->restart_(); }); } From c2902c9671eb0f12bebdfb1a197b93a6556ad7b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 12:33:10 -0500 Subject: [PATCH 255/394] preen --- esphome/components/web_server/web_server.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 2e5d58d375..39d836b9a0 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -403,7 +403,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { static std::string select_state_json_generator(WebServer *web_server, void *source); static std::string select_all_json_generator(WebServer *web_server, void *source); /// Dump the select state with its value as a JSON string. - std::string select_json(select::Select *obj, const std::string &value, JsonDetail start_config); + std::string select_json(select::Select *obj, const char *value, JsonDetail start_config); #endif #ifdef USE_CLIMATE From cf99bab87b915a55388b5b14b0e660eaa8f74b81 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 12:38:12 -0500 Subject: [PATCH 256/394] preen --- esphome/components/ld2410/select/baud_rate_select.cpp | 2 +- esphome/components/web_server/web_server.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/ld2410/select/baud_rate_select.cpp b/esphome/components/ld2410/select/baud_rate_select.cpp index f4e0b90e2e..340bc4705c 100644 --- a/esphome/components/ld2410/select/baud_rate_select.cpp +++ b/esphome/components/ld2410/select/baud_rate_select.cpp @@ -5,7 +5,7 @@ namespace ld2410 { void BaudRateSelect::control(const std::string &value) { this->publish_state(value); - this->parent_->set_baud_rate(state); + this->parent_->set_baud_rate(value); } } // namespace ld2410 diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 75ecc994c3..22dbd40623 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1212,7 +1212,7 @@ std::string WebServer::select_all_json_generator(WebServer *web_server, void *so return web_server->select_json((select::Select *) (source), ((select::Select *) (source))->current_option(), DETAIL_ALL); } -std::string WebServer::select_json(select::Select *obj, const std::string &value, JsonDetail start_config) { +std::string WebServer::select_json(select::Select *obj, const char *value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); From 5f4f6ced3223b2e104703850181b2476bdc795ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 12:39:18 -0500 Subject: [PATCH 257/394] preen --- esphome/components/ld2410/select/distance_resolution_select.cpp | 2 +- esphome/components/ld2412/select/baud_rate_select.cpp | 2 +- esphome/components/ld2412/select/distance_resolution_select.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/ld2410/select/distance_resolution_select.cpp b/esphome/components/ld2410/select/distance_resolution_select.cpp index eef34bda63..6c22ba2ff8 100644 --- a/esphome/components/ld2410/select/distance_resolution_select.cpp +++ b/esphome/components/ld2410/select/distance_resolution_select.cpp @@ -5,7 +5,7 @@ namespace ld2410 { void DistanceResolutionSelect::control(const std::string &value) { this->publish_state(value); - this->parent_->set_distance_resolution(state); + this->parent_->set_distance_resolution(value); } } // namespace ld2410 diff --git a/esphome/components/ld2412/select/baud_rate_select.cpp b/esphome/components/ld2412/select/baud_rate_select.cpp index 2291a81896..5dc30dbd3b 100644 --- a/esphome/components/ld2412/select/baud_rate_select.cpp +++ b/esphome/components/ld2412/select/baud_rate_select.cpp @@ -5,7 +5,7 @@ namespace ld2412 { void BaudRateSelect::control(const std::string &value) { this->publish_state(value); - this->parent_->set_baud_rate(state); + this->parent_->set_baud_rate(value); } } // namespace ld2412 diff --git a/esphome/components/ld2412/select/distance_resolution_select.cpp b/esphome/components/ld2412/select/distance_resolution_select.cpp index a282215fbd..03d9c7c58e 100644 --- a/esphome/components/ld2412/select/distance_resolution_select.cpp +++ b/esphome/components/ld2412/select/distance_resolution_select.cpp @@ -5,7 +5,7 @@ namespace ld2412 { void DistanceResolutionSelect::control(const std::string &value) { this->publish_state(value); - this->parent_->set_distance_resolution(state); + this->parent_->set_distance_resolution(value); } } // namespace ld2412 From 29887e1da56b43194c4cb3e936c80a25bfbd8d0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 12:43:50 -0500 Subject: [PATCH 258/394] preen --- esphome/components/copy/select/copy_select.cpp | 2 +- esphome/components/ld2450/select/baud_rate_select.cpp | 2 +- esphome/components/ld2450/select/zone_type_select.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/copy/select/copy_select.cpp b/esphome/components/copy/select/copy_select.cpp index fde7e3d92d..a424ff3d7f 100644 --- a/esphome/components/copy/select/copy_select.cpp +++ b/esphome/components/copy/select/copy_select.cpp @@ -12,7 +12,7 @@ void CopySelect::setup() { traits.set_options(source_->traits.get_options()); if (source_->has_state()) - this->publish_state(source_->current_option()); + this->publish_state(source_->active_index().value()); } void CopySelect::dump_config() { LOG_SELECT("", "Copy Select", this); } diff --git a/esphome/components/ld2450/select/baud_rate_select.cpp b/esphome/components/ld2450/select/baud_rate_select.cpp index 06439aaa75..f40d75e827 100644 --- a/esphome/components/ld2450/select/baud_rate_select.cpp +++ b/esphome/components/ld2450/select/baud_rate_select.cpp @@ -5,7 +5,7 @@ namespace ld2450 { void BaudRateSelect::control(const std::string &value) { this->publish_state(value); - this->parent_->set_baud_rate(state); + this->parent_->set_baud_rate(value); } } // namespace ld2450 diff --git a/esphome/components/ld2450/select/zone_type_select.cpp b/esphome/components/ld2450/select/zone_type_select.cpp index a9f6155142..b7c1024ec2 100644 --- a/esphome/components/ld2450/select/zone_type_select.cpp +++ b/esphome/components/ld2450/select/zone_type_select.cpp @@ -5,7 +5,7 @@ namespace ld2450 { void ZoneTypeSelect::control(const std::string &value) { this->publish_state(value); - this->parent_->set_zone_type(state); + this->parent_->set_zone_type(value); } } // namespace ld2450 From d1adf79fc3b466c5197867cac7443f825ffe8c8d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 12:45:41 -0500 Subject: [PATCH 259/394] preen --- esphome/components/select/select.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 81c8f68362..fbbecaa5c1 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -67,11 +67,6 @@ class Select : public EntityBase { void add_on_state_callback(std::function &&callback); - protected: - friend class SelectCall; - - size_t active_index_{0}; - /** Set the value of the select by index, this is an optional virtual method. * * This method is called by the SelectCall when the index is already known. @@ -82,6 +77,11 @@ class Select : public EntityBase { */ virtual void control(size_t index) { this->control(this->option_at(index)); } + protected: + friend class SelectCall; + + size_t active_index_{0}; + /** Set the value of the select, this is a virtual method that each select integration must implement. * * This method is called by the SelectCall. From a02b90129d6f7b5d2303ae1a1b614c0222715fb9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 13:00:02 -0500 Subject: [PATCH 260/394] preen --- esphome/components/web_server/web_server.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 22dbd40623..c760d23fb6 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1185,7 +1185,8 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->select_json(obj, obj->current_option(), detail); + const char *value = obj->has_state() ? obj->current_option() : ""; + std::string data = this->select_json(obj, value, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1205,12 +1206,14 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM request->send(404); } std::string WebServer::select_state_json_generator(WebServer *web_server, void *source) { - return web_server->select_json((select::Select *) (source), ((select::Select *) (source))->current_option(), - DETAIL_STATE); + auto *obj = (select::Select *) (source); + const char *value = obj->has_state() ? obj->current_option() : ""; + return web_server->select_json(obj, value, DETAIL_STATE); } std::string WebServer::select_all_json_generator(WebServer *web_server, void *source) { - return web_server->select_json((select::Select *) (source), ((select::Select *) (source))->current_option(), - DETAIL_ALL); + auto *obj = (select::Select *) (source); + const char *value = obj->has_state() ? obj->current_option() : ""; + return web_server->select_json(obj, value, DETAIL_ALL); } std::string WebServer::select_json(select::Select *obj, const char *value, JsonDetail start_config) { json::JsonBuilder builder; From 58a517afa6d68d999a39e8ecc45114b3b06d40d3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 13:01:32 -0500 Subject: [PATCH 261/394] preen --- esphome/components/api/api_connection.cpp | 3 ++- esphome/components/web_server/web_server.cpp | 9 +++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 69c7efea32..52700f0500 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -877,7 +877,8 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection bool is_single) { auto *select = static_cast(entity); SelectStateResponse resp; - resp.set_state(StringRef(select->current_option())); + const char *state = select->has_state() ? select->current_option() : ""; + resp.set_state(StringRef(state)); resp.missing_state = !select->has_state(); return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index c760d23fb6..9aac55d54f 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1185,8 +1185,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - const char *value = obj->has_state() ? obj->current_option() : ""; - std::string data = this->select_json(obj, value, detail); + std::string data = this->select_json(obj, obj->has_state() ? obj->current_option() : "", detail); request->send(200, "application/json", data.c_str()); return; } @@ -1207,13 +1206,11 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM } std::string WebServer::select_state_json_generator(WebServer *web_server, void *source) { auto *obj = (select::Select *) (source); - const char *value = obj->has_state() ? obj->current_option() : ""; - return web_server->select_json(obj, value, DETAIL_STATE); + return web_server->select_json(obj, obj->has_state() ? obj->current_option() : "", DETAIL_STATE); } std::string WebServer::select_all_json_generator(WebServer *web_server, void *source) { auto *obj = (select::Select *) (source); - const char *value = obj->has_state() ? obj->current_option() : ""; - return web_server->select_json(obj, value, DETAIL_ALL); + return web_server->select_json(obj, obj->has_state() ? obj->current_option() : "", DETAIL_ALL); } std::string WebServer::select_json(select::Select *obj, const char *value, JsonDetail start_config) { json::JsonBuilder builder; From f6aee64ec1f81d2b2d37946e3a7d0342483312ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 13:02:37 -0500 Subject: [PATCH 262/394] preen --- esphome/components/api/api_connection.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 52700f0500..69c7efea32 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -877,8 +877,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection bool is_single) { auto *select = static_cast(entity); SelectStateResponse resp; - const char *state = select->has_state() ? select->current_option() : ""; - resp.set_state(StringRef(state)); + resp.set_state(StringRef(select->current_option())); resp.missing_state = !select->has_state(); return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } From fd8726b4794707865ef7b86541ae41369940bcc3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 13:07:03 -0500 Subject: [PATCH 263/394] comment it --- esphome/components/select/select.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index fbbecaa5c1..801074a4d5 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -84,7 +84,13 @@ class Select : public EntityBase { /** Set the value of the select, this is a virtual method that each select integration must implement. * - * This method is called by the SelectCall. + * This method is called by control(size_t) when not overridden, or directly by external code. + * All existing integrations implement this method. New integrations can optionally override + * control(size_t) instead to work with indices directly and avoid string conversions. + * + * Delegation chain: + * - SelectCall::perform() → control(size_t) → [if not overridden] → control(string) + * - External code → control(string) → publish_state(string) → publish_state(size_t) * * @param value The value as validated by the SelectCall. */ From b6d178b8c1d78d5335ec1fb936f72b37373ede66 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 13:12:28 -0500 Subject: [PATCH 264/394] cleanups --- esphome/components/copy/select/copy_select.cpp | 4 ++-- esphome/components/copy/select/copy_select.h | 2 +- esphome/components/ld2410/ld2410.cpp | 8 ++++---- esphome/components/ld2410/ld2410.h | 4 ++-- .../components/ld2410/select/baud_rate_select.cpp | 6 +++--- esphome/components/ld2410/select/baud_rate_select.h | 2 +- .../ld2410/select/distance_resolution_select.cpp | 6 +++--- .../ld2410/select/distance_resolution_select.h | 2 +- .../ld2410/select/light_out_control_select.h | 2 +- esphome/components/ld2412/ld2412.cpp | 8 ++++---- esphome/components/ld2412/ld2412.h | 4 ++-- .../components/ld2412/select/baud_rate_select.cpp | 6 +++--- esphome/components/ld2412/select/baud_rate_select.h | 2 +- .../ld2412/select/distance_resolution_select.cpp | 6 +++--- .../ld2412/select/distance_resolution_select.h | 2 +- .../ld2412/select/light_out_control_select.h | 2 +- esphome/components/ld2420/ld2420.cpp | 2 +- esphome/components/ld2420/ld2420.h | 2 +- .../ld2420/select/operating_mode_select.cpp | 6 +++--- .../components/ld2420/select/operating_mode_select.h | 2 +- esphome/components/ld2450/ld2450.cpp | 6 +++--- esphome/components/ld2450/ld2450.h | 4 ++-- .../components/ld2450/select/baud_rate_select.cpp | 6 +++--- esphome/components/ld2450/select/baud_rate_select.h | 2 +- .../components/ld2450/select/zone_type_select.cpp | 6 +++--- esphome/components/ld2450/select/zone_type_select.h | 2 +- .../components/logger/select/logger_level_select.cpp | 7 +------ .../components/logger/select/logger_level_select.h | 2 +- .../components/template/select/template_select.cpp | 12 +++++------- esphome/components/template/select/template_select.h | 2 +- 30 files changed, 60 insertions(+), 67 deletions(-) diff --git a/esphome/components/copy/select/copy_select.cpp b/esphome/components/copy/select/copy_select.cpp index a424ff3d7f..e45338e785 100644 --- a/esphome/components/copy/select/copy_select.cpp +++ b/esphome/components/copy/select/copy_select.cpp @@ -17,9 +17,9 @@ void CopySelect::setup() { void CopySelect::dump_config() { LOG_SELECT("", "Copy Select", this); } -void CopySelect::control(const std::string &value) { +void CopySelect::control(size_t index) { auto call = source_->make_call(); - call.set_option(value); + call.set_index(index); call.perform(); } diff --git a/esphome/components/copy/select/copy_select.h b/esphome/components/copy/select/copy_select.h index fb0aee86f6..bd74a93e82 100644 --- a/esphome/components/copy/select/copy_select.h +++ b/esphome/components/copy/select/copy_select.h @@ -13,7 +13,7 @@ class CopySelect : public select::Select, public Component { void dump_config() override; protected: - void control(const std::string &value) override; + void control(size_t index) override; select::Select *source_; }; diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index ecb2920007..608882565f 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -626,16 +626,16 @@ void LD2410Component::set_bluetooth(bool enable) { this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); } -void LD2410Component::set_distance_resolution(const std::string &state) { +void LD2410Component::set_distance_resolution(const char *state) { this->set_config_mode_(true); - const uint8_t cmd_value[2] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state.c_str()), 0x00}; + const uint8_t cmd_value[2] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00}; this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value)); this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); } -void LD2410Component::set_baud_rate(const std::string &state) { +void LD2410Component::set_baud_rate(const char *state) { this->set_config_mode_(true); - const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state.c_str()), 0x00}; + const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value)); this->set_timeout(200, [this]() { this->restart_(); }); } diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h index 54fe1ce14d..52cf76b5b6 100644 --- a/esphome/components/ld2410/ld2410.h +++ b/esphome/components/ld2410/ld2410.h @@ -98,8 +98,8 @@ class LD2410Component : public Component, public uart::UARTDevice { void read_all_info(); void restart_and_read_all_info(); void set_bluetooth(bool enable); - void set_distance_resolution(const std::string &state); - void set_baud_rate(const std::string &state); + void set_distance_resolution(const char *state); + void set_baud_rate(const char *state); void factory_reset(); protected: diff --git a/esphome/components/ld2410/select/baud_rate_select.cpp b/esphome/components/ld2410/select/baud_rate_select.cpp index 340bc4705c..6da7c1d5f5 100644 --- a/esphome/components/ld2410/select/baud_rate_select.cpp +++ b/esphome/components/ld2410/select/baud_rate_select.cpp @@ -3,9 +3,9 @@ namespace esphome { namespace ld2410 { -void BaudRateSelect::control(const std::string &value) { - this->publish_state(value); - this->parent_->set_baud_rate(value); +void BaudRateSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_baud_rate(this->option_at(index)); } } // namespace ld2410 diff --git a/esphome/components/ld2410/select/baud_rate_select.h b/esphome/components/ld2410/select/baud_rate_select.h index 3827b6a48a..9385c8cf7e 100644 --- a/esphome/components/ld2410/select/baud_rate_select.h +++ b/esphome/components/ld2410/select/baud_rate_select.h @@ -11,7 +11,7 @@ class BaudRateSelect : public select::Select, public Parented { BaudRateSelect() = default; protected: - void control(const std::string &value) override; + void control(size_t index) override; }; } // namespace ld2410 diff --git a/esphome/components/ld2410/select/distance_resolution_select.cpp b/esphome/components/ld2410/select/distance_resolution_select.cpp index 6c22ba2ff8..4fc4c5af02 100644 --- a/esphome/components/ld2410/select/distance_resolution_select.cpp +++ b/esphome/components/ld2410/select/distance_resolution_select.cpp @@ -3,9 +3,9 @@ namespace esphome { namespace ld2410 { -void DistanceResolutionSelect::control(const std::string &value) { - this->publish_state(value); - this->parent_->set_distance_resolution(value); +void DistanceResolutionSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_distance_resolution(this->option_at(index)); } } // namespace ld2410 diff --git a/esphome/components/ld2410/select/distance_resolution_select.h b/esphome/components/ld2410/select/distance_resolution_select.h index d6affb1020..1a04f843a6 100644 --- a/esphome/components/ld2410/select/distance_resolution_select.h +++ b/esphome/components/ld2410/select/distance_resolution_select.h @@ -11,7 +11,7 @@ class DistanceResolutionSelect : public select::Select, public Parentedset_timeout(200, [this]() { this->restart_and_read_all_info(); }); } -void LD2412Component::set_distance_resolution(const std::string &state) { +void LD2412Component::set_distance_resolution(const char *state) { this->set_config_mode_(true); - const uint8_t cmd_value[6] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state.c_str()), 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t cmd_value[6] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00, 0x00, 0x00, 0x00, 0x00}; this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value)); this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); } -void LD2412Component::set_baud_rate(const std::string &state) { +void LD2412Component::set_baud_rate(const char *state) { this->set_config_mode_(true); - const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state.c_str()), 0x00}; + const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value)); this->set_timeout(200, [this]() { this->restart_(); }); } diff --git a/esphome/components/ld2412/ld2412.h b/esphome/components/ld2412/ld2412.h index 41f96ab301..2bed34bdd8 100644 --- a/esphome/components/ld2412/ld2412.h +++ b/esphome/components/ld2412/ld2412.h @@ -99,8 +99,8 @@ class LD2412Component : public Component, public uart::UARTDevice { void read_all_info(); void restart_and_read_all_info(); void set_bluetooth(bool enable); - void set_distance_resolution(const std::string &state); - void set_baud_rate(const std::string &state); + void set_distance_resolution(const char *state); + void set_baud_rate(const char *state); void factory_reset(); void start_dynamic_background_correction(); diff --git a/esphome/components/ld2412/select/baud_rate_select.cpp b/esphome/components/ld2412/select/baud_rate_select.cpp index 5dc30dbd3b..7bc4683853 100644 --- a/esphome/components/ld2412/select/baud_rate_select.cpp +++ b/esphome/components/ld2412/select/baud_rate_select.cpp @@ -3,9 +3,9 @@ namespace esphome { namespace ld2412 { -void BaudRateSelect::control(const std::string &value) { - this->publish_state(value); - this->parent_->set_baud_rate(value); +void BaudRateSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_baud_rate(this->option_at(index)); } } // namespace ld2412 diff --git a/esphome/components/ld2412/select/baud_rate_select.h b/esphome/components/ld2412/select/baud_rate_select.h index 2ae33551fb..ffe0329341 100644 --- a/esphome/components/ld2412/select/baud_rate_select.h +++ b/esphome/components/ld2412/select/baud_rate_select.h @@ -11,7 +11,7 @@ class BaudRateSelect : public select::Select, public Parented { BaudRateSelect() = default; protected: - void control(const std::string &value) override; + void control(size_t index) override; }; } // namespace ld2412 diff --git a/esphome/components/ld2412/select/distance_resolution_select.cpp b/esphome/components/ld2412/select/distance_resolution_select.cpp index 03d9c7c58e..5a6f46a071 100644 --- a/esphome/components/ld2412/select/distance_resolution_select.cpp +++ b/esphome/components/ld2412/select/distance_resolution_select.cpp @@ -3,9 +3,9 @@ namespace esphome { namespace ld2412 { -void DistanceResolutionSelect::control(const std::string &value) { - this->publish_state(value); - this->parent_->set_distance_resolution(value); +void DistanceResolutionSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_distance_resolution(this->option_at(index)); } } // namespace ld2412 diff --git a/esphome/components/ld2412/select/distance_resolution_select.h b/esphome/components/ld2412/select/distance_resolution_select.h index 0658f5d1a7..842f63b7b1 100644 --- a/esphome/components/ld2412/select/distance_resolution_select.h +++ b/esphome/components/ld2412/select/distance_resolution_select.h @@ -11,7 +11,7 @@ class DistanceResolutionSelect : public select::Select, public Parentedtotal_sample_number_counter); } -void LD2420Component::set_operating_mode(const std::string &state) { +void LD2420Component::set_operating_mode(const char *state) { // If unsupported firmware ignore mode select if (ld2420::get_firmware_int(firmware_ver_) >= CALIBRATE_VERSION_MIN) { this->current_operating_mode = find_uint8(OP_MODE_BY_STR, state); diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 812c408cfd..128baab604 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -107,7 +107,7 @@ class LD2420Component : public Component, public uart::UARTDevice { int send_cmd_from_array(CmdFrameT cmd_frame); void report_gate_data(); void handle_cmd_error(uint8_t error); - void set_operating_mode(const std::string &state); + void set_operating_mode(const char *state); void auto_calibrate_sensitivity(); void update_radar_data(uint16_t const *gate_energy, uint8_t sample_number); uint8_t set_config_mode(bool enable); diff --git a/esphome/components/ld2420/select/operating_mode_select.cpp b/esphome/components/ld2420/select/operating_mode_select.cpp index 2d576e7cc6..5bf80b33c9 100644 --- a/esphome/components/ld2420/select/operating_mode_select.cpp +++ b/esphome/components/ld2420/select/operating_mode_select.cpp @@ -7,9 +7,9 @@ namespace ld2420 { static const char *const TAG = "ld2420.select"; -void LD2420Select::control(const std::string &value) { - this->publish_state(value); - this->parent_->set_operating_mode(value); +void LD2420Select::control(size_t index) { + this->publish_state(index); + this->parent_->set_operating_mode(this->option_at(index)); } } // namespace ld2420 diff --git a/esphome/components/ld2420/select/operating_mode_select.h b/esphome/components/ld2420/select/operating_mode_select.h index 317b2af8c0..f59eb33432 100644 --- a/esphome/components/ld2420/select/operating_mode_select.h +++ b/esphome/components/ld2420/select/operating_mode_select.h @@ -11,7 +11,7 @@ class LD2420Select : public Component, public select::Select, public Parentedset_config_mode_(true); const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value)); @@ -798,8 +798,8 @@ void LD2450Component::set_baud_rate(const std::string &state) { } // Set Zone Type - one of: Disabled, Detection, Filter -void LD2450Component::set_zone_type(const std::string &state) { - ESP_LOGV(TAG, "Set zone type: %s", state.c_str()); +void LD2450Component::set_zone_type(const char *state) { + ESP_LOGV(TAG, "Set zone type: %s", state); uint8_t zone_type = find_uint8(ZONE_TYPE_BY_STR, state); this->zone_type_ = zone_type; this->send_set_zone_command_(); diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 9faa189019..44b63be444 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -115,8 +115,8 @@ class LD2450Component : public Component, public uart::UARTDevice { void restart_and_read_all_info(); void set_bluetooth(bool enable); void set_multi_target(bool enable); - void set_baud_rate(const std::string &state); - void set_zone_type(const std::string &state); + void set_baud_rate(const char *state); + void set_zone_type(const char *state); void publish_zone_type(); void factory_reset(); #ifdef USE_TEXT_SENSOR diff --git a/esphome/components/ld2450/select/baud_rate_select.cpp b/esphome/components/ld2450/select/baud_rate_select.cpp index f40d75e827..754972214e 100644 --- a/esphome/components/ld2450/select/baud_rate_select.cpp +++ b/esphome/components/ld2450/select/baud_rate_select.cpp @@ -3,9 +3,9 @@ namespace esphome { namespace ld2450 { -void BaudRateSelect::control(const std::string &value) { - this->publish_state(value); - this->parent_->set_baud_rate(value); +void BaudRateSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_baud_rate(this->option_at(index)); } } // namespace ld2450 diff --git a/esphome/components/ld2450/select/baud_rate_select.h b/esphome/components/ld2450/select/baud_rate_select.h index 04fe65b4fd..22810d5f13 100644 --- a/esphome/components/ld2450/select/baud_rate_select.h +++ b/esphome/components/ld2450/select/baud_rate_select.h @@ -11,7 +11,7 @@ class BaudRateSelect : public select::Select, public Parented { BaudRateSelect() = default; protected: - void control(const std::string &value) override; + void control(size_t index) override; }; } // namespace ld2450 diff --git a/esphome/components/ld2450/select/zone_type_select.cpp b/esphome/components/ld2450/select/zone_type_select.cpp index b7c1024ec2..1111428c7c 100644 --- a/esphome/components/ld2450/select/zone_type_select.cpp +++ b/esphome/components/ld2450/select/zone_type_select.cpp @@ -3,9 +3,9 @@ namespace esphome { namespace ld2450 { -void ZoneTypeSelect::control(const std::string &value) { - this->publish_state(value); - this->parent_->set_zone_type(value); +void ZoneTypeSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_zone_type(this->option_at(index)); } } // namespace ld2450 diff --git a/esphome/components/ld2450/select/zone_type_select.h b/esphome/components/ld2450/select/zone_type_select.h index 8aafeb6beb..fc95ec1021 100644 --- a/esphome/components/ld2450/select/zone_type_select.h +++ b/esphome/components/ld2450/select/zone_type_select.h @@ -11,7 +11,7 @@ class ZoneTypeSelect : public select::Select, public Parented { ZoneTypeSelect() = default; protected: - void control(const std::string &value) override; + void control(size_t index) override; }; } // namespace ld2450 diff --git a/esphome/components/logger/select/logger_level_select.cpp b/esphome/components/logger/select/logger_level_select.cpp index 5537df570c..e2ec28a390 100644 --- a/esphome/components/logger/select/logger_level_select.cpp +++ b/esphome/components/logger/select/logger_level_select.cpp @@ -14,11 +14,6 @@ void LoggerLevelSelect::setup() { this->publish_state(this->parent_->get_log_level()); } -void LoggerLevelSelect::control(const std::string &value) { - const auto index = this->index_of(value); - if (!index) - return; - this->parent_->set_log_level(index_to_level(index.value())); -} +void LoggerLevelSelect::control(size_t index) { this->parent_->set_log_level(index_to_level(index)); } } // namespace esphome::logger diff --git a/esphome/components/logger/select/logger_level_select.h b/esphome/components/logger/select/logger_level_select.h index 0631eca45d..950edd29ac 100644 --- a/esphome/components/logger/select/logger_level_select.h +++ b/esphome/components/logger/select/logger_level_select.h @@ -9,7 +9,7 @@ class LoggerLevelSelect : public Component, public select::Select, public Parent public: void publish_state(int level); void setup() override; - void control(const std::string &value) override; + void control(size_t index) override; protected: // Convert log level to option index (skip CONFIG at level 4) diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 03ef1d482b..112f24e919 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -41,16 +41,14 @@ void TemplateSelect::update() { } } -void TemplateSelect::control(const std::string &value) { - this->set_trigger_->trigger(value); +void TemplateSelect::control(size_t index) { + this->set_trigger_->trigger(std::string(this->option_at(index))); if (this->optimistic_) - this->publish_state(value); + this->publish_state(index); - if (this->restore_value_) { - auto index = this->index_of(value); - this->pref_.save(&index.value()); - } + if (this->restore_value_) + this->pref_.save(&index); } void TemplateSelect::dump_config() { diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 1c33153872..2dad059ade 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -24,7 +24,7 @@ class TemplateSelect : public select::Select, public PollingComponent { void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } protected: - void control(const std::string &value) override; + void control(size_t index) override; bool optimistic_ = false; size_t initial_option_index_{0}; bool restore_value_ = false; From 6dff2d6240be90cfcc14cb0c69d0a7d4050d04a7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 13:17:25 -0500 Subject: [PATCH 265/394] cleanups --- esphome/components/ld2410/select/light_out_control_select.cpp | 4 ++-- esphome/components/ld2412/select/light_out_control_select.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/ld2410/select/light_out_control_select.cpp b/esphome/components/ld2410/select/light_out_control_select.cpp index ac23248a64..25d7ced666 100644 --- a/esphome/components/ld2410/select/light_out_control_select.cpp +++ b/esphome/components/ld2410/select/light_out_control_select.cpp @@ -3,8 +3,8 @@ namespace esphome { namespace ld2410 { -void LightOutControlSelect::control(const std::string &value) { - this->publish_state(value); +void LightOutControlSelect::control(size_t index) { + this->publish_state(index); this->parent_->set_light_out_control(); } diff --git a/esphome/components/ld2412/select/light_out_control_select.cpp b/esphome/components/ld2412/select/light_out_control_select.cpp index c331729d40..cfbc7f7d7c 100644 --- a/esphome/components/ld2412/select/light_out_control_select.cpp +++ b/esphome/components/ld2412/select/light_out_control_select.cpp @@ -3,8 +3,8 @@ namespace esphome { namespace ld2412 { -void LightOutControlSelect::control(const std::string &value) { - this->publish_state(value); +void LightOutControlSelect::control(size_t index) { + this->publish_state(index); this->parent_->set_light_out_control(); } From 2e6dab89ff78fce39fe8cbbb5a6ef7df927fa464 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 13:19:45 -0500 Subject: [PATCH 266/394] preen --- esphome/components/select/select.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 63192623d3..0c05a7b3bf 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -63,18 +63,16 @@ optional Select::index_of(const char *option) const { optional Select::active_index() const { if (this->has_state()) { return this->active_index_; - } else { - return {}; } + return {}; } optional Select::at(size_t index) const { if (this->has_index(index)) { const auto &options = traits.get_options(); return std::string(options.at(index)); - } else { - return {}; } + return {}; } const char *Select::option_at(size_t index) const { return traits.get_options().at(index); } From ad5752f68e4c543ae57e509ffec67c0c964bbca1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 13:25:31 -0500 Subject: [PATCH 267/394] give people time to migrate since we can --- esphome/components/select/select.cpp | 1 + esphome/components/select/select.h | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 0c05a7b3bf..e495ab4a44 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -26,6 +26,7 @@ void Select::publish_state(size_t index) { const char *option = this->option_at(index); this->set_has_state(true); this->active_index_ = index; + this->state = option; // Update deprecated member for backward compatibility ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", this->get_name().c_str(), option, index); // Callback signature requires std::string, create temporary for compatibility this->state_callback_.call(std::string(option), index); diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 801074a4d5..26e5c6dbde 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -32,6 +32,9 @@ class Select : public EntityBase { public: SelectTraits traits; + /// @deprecated Use current_option() instead. This member will be removed in a future release. + __attribute__((deprecated("Use current_option() instead of .state"))) std::string state{}; + void publish_state(const std::string &state); void publish_state(const char *state); void publish_state(size_t index); @@ -82,11 +85,14 @@ class Select : public EntityBase { size_t active_index_{0}; - /** Set the value of the select, this is a virtual method that each select integration must implement. + /** Set the value of the select, this is a virtual method that each select integration can implement. * * This method is called by control(size_t) when not overridden, or directly by external code. - * All existing integrations implement this method. New integrations can optionally override - * control(size_t) instead to work with indices directly and avoid string conversions. + * Integrations can either: + * 1. Override this method to handle string-based control (traditional approach) + * 2. Override control(size_t) instead to work with indices directly (recommended) + * + * Default implementation converts to index and calls control(size_t). * * Delegation chain: * - SelectCall::perform() → control(size_t) → [if not overridden] → control(string) @@ -94,7 +100,12 @@ class Select : public EntityBase { * * @param value The value as validated by the SelectCall. */ - virtual void control(const std::string &value) = 0; + virtual void control(const std::string &value) { + auto index = this->index_of(value); + if (index.has_value()) { + this->control(index.value()); + } + } CallbackManager state_callback_; }; From 7d2ebabec78d080b84c948c78a7f0fe5d3341848 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 13:28:27 -0500 Subject: [PATCH 268/394] give people time to migrate since we can --- esphome/components/select/select.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index e495ab4a44..fae485709e 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -26,7 +26,10 @@ void Select::publish_state(size_t index) { const char *option = this->option_at(index); this->set_has_state(true); this->active_index_ = index; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" this->state = option; // Update deprecated member for backward compatibility +#pragma GCC diagnostic pop ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", this->get_name().c_str(), option, index); // Callback signature requires std::string, create temporary for compatibility this->state_callback_.call(std::string(option), index); From d496676c8436f354880fd661cc78fc8da5b9a56a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 13:30:22 -0500 Subject: [PATCH 269/394] preen --- esphome/components/select/select.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 26e5c6dbde..22481941b4 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -32,8 +32,12 @@ class Select : public EntityBase { public: SelectTraits traits; - /// @deprecated Use current_option() instead. This member will be removed in a future release. - __attribute__((deprecated("Use current_option() instead of .state"))) std::string state{}; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + /// @deprecated Use current_option() instead. This member will be removed in ESPHome 2026.5.0. + __attribute__((deprecated("Use current_option() instead of .state. Will be removed in 2026.5.0"))) + std::string state{}; +#pragma GCC diagnostic pop void publish_state(const std::string &state); void publish_state(const char *state); From 849483eb3bf91236db77a839360fbce6678a8946 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 13:35:35 -0500 Subject: [PATCH 270/394] silience warning --- esphome/components/select/select.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 22481941b4..030646c1ad 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -37,6 +37,9 @@ class Select : public EntityBase { /// @deprecated Use current_option() instead. This member will be removed in ESPHome 2026.5.0. __attribute__((deprecated("Use current_option() instead of .state. Will be removed in 2026.5.0"))) std::string state{}; + + Select() = default; + ~Select() = default; #pragma GCC diagnostic pop void publish_state(const std::string &state); From 54c536cbe29a53938b66f4cf3d7a5c0cf0e81f95 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 13:40:33 -0500 Subject: [PATCH 271/394] missed some --- esphome/components/ld2420/ld2420.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index 44106ffdd2..b48c336d4e 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -131,8 +131,8 @@ static const uint8_t CMD_FRAME_STATUS = 7; static const uint8_t CMD_ERROR_WORD = 8; static const uint8_t ENERGY_SENSOR_START = 9; static const uint8_t CALIBRATE_REPORT_INTERVAL = 4; -static const std::string OP_NORMAL_MODE_STRING = "Normal"; -static const std::string OP_SIMPLE_MODE_STRING = "Simple"; +static const char *const OP_NORMAL_MODE_STRING = "Normal"; +static const char *const OP_SIMPLE_MODE_STRING = "Simple"; // Memory-efficient lookup tables struct StringToUint8 { From c02d316866e0cf6d98dd1d682c155d03bddbd8ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 14:07:49 -0500 Subject: [PATCH 272/394] tidy --- esphome/components/select/select_call.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index 144090e21f..b9253ed6a2 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -80,7 +80,7 @@ void SelectCall::perform() { target_index = 0; } else if (this->operation_ == SELECT_OP_LAST) { target_index = options.size() - 1; - } else if (this->operation_ == SELECT_OP_NEXT || this->operation_ == SELECT_OP_PREVIOUS) { + } else { // SELECT_OP_NEXT or SELECT_OP_PREVIOUS auto cycle = this->cycle_; ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, this->operation_ == SELECT_OP_NEXT ? "next" : "previous", cycle ? "" : "out"); From 9f62df14566c930406e82f5d04c5954291ca42d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 14:16:43 -0500 Subject: [PATCH 273/394] break it out, logic was too hard to follow --- esphome/components/select/select_call.cpp | 112 ++++++++++++---------- esphome/components/select/select_call.h | 2 + 2 files changed, 65 insertions(+), 49 deletions(-) diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index b9253ed6a2..615cc8d057 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -46,67 +46,81 @@ SelectCall &SelectCall::with_index(size_t index) { return *this; } +optional SelectCall::calculate_target_index_(const char *name) { + const auto &options = this->parent_->traits.get_options(); + if (options.empty()) { + ESP_LOGW(TAG, "'%s' - Cannot perform SelectCall, select has no options", name); + return {}; + } + + if (this->operation_ == SELECT_OP_FIRST) { + return 0; + } + + if (this->operation_ == SELECT_OP_LAST) { + return options.size() - 1; + } + + if (this->operation_ == SELECT_OP_SET || this->operation_ == SELECT_OP_SET_INDEX) { + if (!this->index_.has_value()) { + ESP_LOGW(TAG, "'%s' - No option value set for SelectCall", name); + return {}; + } + auto idx = this->index_.value(); + if (idx >= options.size()) { + ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", name, idx); + return {}; + } + if (this->operation_ == SELECT_OP_SET) { + ESP_LOGD(TAG, "'%s' - Setting", name); + } + return idx; + } + + // SELECT_OP_NEXT or SELECT_OP_PREVIOUS + ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, this->operation_ == SELECT_OP_NEXT ? "next" : "previous", + this->cycle_ ? "" : "out"); + + const auto size = options.size(); + if (!this->parent_->has_state()) { + return this->operation_ == SELECT_OP_NEXT ? 0 : size - 1; + } + + // Use cached active_index_ instead of index_of() lookup + const auto index = this->parent_->active_index_; + if (this->cycle_) { + return (size + index + (this->operation_ == SELECT_OP_NEXT ? +1 : -1)) % size; + } + + if (this->operation_ == SELECT_OP_PREVIOUS && index > 0) { + return index - 1; + } + + if (this->operation_ == SELECT_OP_NEXT && index < size - 1) { + return index + 1; + } + + return {}; // Can't navigate further without cycling +} + void SelectCall::perform() { auto *parent = this->parent_; const auto *name = parent->get_name().c_str(); - const auto &traits = parent->traits; - const auto &options = traits.get_options(); if (this->operation_ == SELECT_OP_NONE) { ESP_LOGW(TAG, "'%s' - SelectCall performed without selecting an operation", name); return; } - if (options.empty()) { - ESP_LOGW(TAG, "'%s' - Cannot perform SelectCall, select has no options", name); + + auto target_index = this->calculate_target_index_(name); + if (!target_index.has_value()) { return; } - size_t target_index; - - if (this->operation_ == SELECT_OP_SET || this->operation_ == SELECT_OP_SET_INDEX) { - if (this->operation_ == SELECT_OP_SET) { - ESP_LOGD(TAG, "'%s' - Setting", name); - } - if (!this->index_.has_value()) { - ESP_LOGW(TAG, "'%s' - No option value set for SelectCall", name); - return; - } - if (this->index_.value() >= options.size()) { - ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", name, this->index_.value()); - return; - } - target_index = this->index_.value(); - } else if (this->operation_ == SELECT_OP_FIRST) { - target_index = 0; - } else if (this->operation_ == SELECT_OP_LAST) { - target_index = options.size() - 1; - } else { // SELECT_OP_NEXT or SELECT_OP_PREVIOUS - auto cycle = this->cycle_; - ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, this->operation_ == SELECT_OP_NEXT ? "next" : "previous", - cycle ? "" : "out"); - if (!parent->has_state()) { - target_index = this->operation_ == SELECT_OP_NEXT ? 0 : options.size() - 1; - } else { - // Use cached active_index_ instead of index_of() lookup - auto index = parent->active_index_; - auto size = options.size(); - if (cycle) { - target_index = (size + index + (this->operation_ == SELECT_OP_NEXT ? +1 : -1)) % size; - } else { - if (this->operation_ == SELECT_OP_PREVIOUS && index > 0) { - target_index = index - 1; - } else if (this->operation_ == SELECT_OP_NEXT && index < options.size() - 1) { - target_index = index + 1; - } else { - return; - } - } - } - } - // All operations use indices, call control() by index to avoid string conversion - ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, options[target_index]); - parent->control(target_index); + const auto &options = parent->traits.get_options(); + ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, options[target_index.value()]); + parent->control(target_index.value()); } } // namespace select diff --git a/esphome/components/select/select_call.h b/esphome/components/select/select_call.h index a0c63a0e39..89f5156800 100644 --- a/esphome/components/select/select_call.h +++ b/esphome/components/select/select_call.h @@ -38,6 +38,8 @@ class SelectCall { SelectCall &with_index(size_t index); protected: + optional calculate_target_index_(const char *name); + Select *const parent_; optional index_; SelectOperation operation_{SELECT_OP_NONE}; From 867ff200ce9aab3870c20ea6647804065b855c62 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 14:18:56 -0500 Subject: [PATCH 274/394] break it out, logic was too hard to follow --- esphome/components/select/select_call.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index 615cc8d057..45ea241d06 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -49,7 +49,7 @@ SelectCall &SelectCall::with_index(size_t index) { optional SelectCall::calculate_target_index_(const char *name) { const auto &options = this->parent_->traits.get_options(); if (options.empty()) { - ESP_LOGW(TAG, "'%s' - Cannot perform SelectCall, select has no options", name); + ESP_LOGW(TAG, "'%s' - Select has no options", name); return {}; } From 6cab143db29c21b31925e807ef46ce56dbc2a78c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 14:20:28 -0500 Subject: [PATCH 275/394] break it out, logic was too hard to follow --- esphome/components/select/select_call.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index 45ea241d06..19148d6e03 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -63,7 +63,7 @@ optional SelectCall::calculate_target_index_(const char *name) { if (this->operation_ == SELECT_OP_SET || this->operation_ == SELECT_OP_SET_INDEX) { if (!this->index_.has_value()) { - ESP_LOGW(TAG, "'%s' - No option value set for SelectCall", name); + ESP_LOGW(TAG, "'%s' - No option set", name); return {}; } auto idx = this->index_.value(); From 1a9aa23ae95014bae74d02fa3771afd1110bd1b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 14:25:35 -0500 Subject: [PATCH 276/394] force inline --- esphome/components/select/select_call.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/select/select_call.h b/esphome/components/select/select_call.h index 89f5156800..0b83cb143a 100644 --- a/esphome/components/select/select_call.h +++ b/esphome/components/select/select_call.h @@ -38,7 +38,7 @@ class SelectCall { SelectCall &with_index(size_t index); protected: - optional calculate_target_index_(const char *name); + __attribute__((always_inline)) inline optional calculate_target_index_(const char *name); Select *const parent_; optional index_; From f447aaed8d82a3f193286922c9192a4e71b39238 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 14:26:37 -0500 Subject: [PATCH 277/394] force inline --- esphome/components/select/select_call.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index 19148d6e03..ae1e6b2f39 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -117,10 +117,10 @@ void SelectCall::perform() { return; } + auto idx = target_index.value(); // All operations use indices, call control() by index to avoid string conversion - const auto &options = parent->traits.get_options(); - ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, options[target_index.value()]); - parent->control(target_index.value()); + ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, parent->option_at(idx)); + parent->control(idx); } } // namespace select From 567672171a3f77af4a234cbc77d0bb0111a01fc4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 14:28:09 -0500 Subject: [PATCH 278/394] force inline --- esphome/components/select/select_call.cpp | 21 ++++++++++----------- esphome/components/select/select_call.h | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index ae1e6b2f39..ffdadfbc26 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -46,10 +46,10 @@ SelectCall &SelectCall::with_index(size_t index) { return *this; } -optional SelectCall::calculate_target_index_(const char *name) { +optional SelectCall::calculate_target_index_() { const auto &options = this->parent_->traits.get_options(); if (options.empty()) { - ESP_LOGW(TAG, "'%s' - Select has no options", name); + ESP_LOGW(TAG, "'%s' - Select has no options", this->parent_->get_name().c_str()); return {}; } @@ -63,23 +63,23 @@ optional SelectCall::calculate_target_index_(const char *name) { if (this->operation_ == SELECT_OP_SET || this->operation_ == SELECT_OP_SET_INDEX) { if (!this->index_.has_value()) { - ESP_LOGW(TAG, "'%s' - No option set", name); + ESP_LOGW(TAG, "'%s' - No option set", this->parent_->get_name().c_str()); return {}; } auto idx = this->index_.value(); if (idx >= options.size()) { - ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", name, idx); + ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", this->parent_->get_name().c_str(), idx); return {}; } if (this->operation_ == SELECT_OP_SET) { - ESP_LOGD(TAG, "'%s' - Setting", name); + ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); } return idx; } // SELECT_OP_NEXT or SELECT_OP_PREVIOUS - ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, this->operation_ == SELECT_OP_NEXT ? "next" : "previous", - this->cycle_ ? "" : "out"); + ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", this->parent_->get_name().c_str(), + this->operation_ == SELECT_OP_NEXT ? "next" : "previous", this->cycle_ ? "" : "out"); const auto size = options.size(); if (!this->parent_->has_state()) { @@ -105,21 +105,20 @@ optional SelectCall::calculate_target_index_(const char *name) { void SelectCall::perform() { auto *parent = this->parent_; - const auto *name = parent->get_name().c_str(); if (this->operation_ == SELECT_OP_NONE) { - ESP_LOGW(TAG, "'%s' - SelectCall performed without selecting an operation", name); + ESP_LOGW(TAG, "'%s' - SelectCall performed without selecting an operation", parent->get_name().c_str()); return; } - auto target_index = this->calculate_target_index_(name); + auto target_index = this->calculate_target_index_(); if (!target_index.has_value()) { return; } auto idx = target_index.value(); // All operations use indices, call control() by index to avoid string conversion - ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, parent->option_at(idx)); + ESP_LOGD(TAG, "'%s' - Set selected option to: %s", parent->get_name().c_str(), parent->option_at(idx)); parent->control(idx); } diff --git a/esphome/components/select/select_call.h b/esphome/components/select/select_call.h index 0b83cb143a..dc6bc85014 100644 --- a/esphome/components/select/select_call.h +++ b/esphome/components/select/select_call.h @@ -38,7 +38,7 @@ class SelectCall { SelectCall &with_index(size_t index); protected: - __attribute__((always_inline)) inline optional calculate_target_index_(const char *name); + __attribute__((always_inline)) inline optional calculate_target_index_(); Select *const parent_; optional index_; From 3552d2916762adf4ef4f5d1cc13c2dd157703903 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 14:30:58 -0500 Subject: [PATCH 279/394] preen --- esphome/components/select/select_call.cpp | 21 +++++++++++---------- esphome/components/select/select_call.h | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index ffdadfbc26..ae1e6b2f39 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -46,10 +46,10 @@ SelectCall &SelectCall::with_index(size_t index) { return *this; } -optional SelectCall::calculate_target_index_() { +optional SelectCall::calculate_target_index_(const char *name) { const auto &options = this->parent_->traits.get_options(); if (options.empty()) { - ESP_LOGW(TAG, "'%s' - Select has no options", this->parent_->get_name().c_str()); + ESP_LOGW(TAG, "'%s' - Select has no options", name); return {}; } @@ -63,23 +63,23 @@ optional SelectCall::calculate_target_index_() { if (this->operation_ == SELECT_OP_SET || this->operation_ == SELECT_OP_SET_INDEX) { if (!this->index_.has_value()) { - ESP_LOGW(TAG, "'%s' - No option set", this->parent_->get_name().c_str()); + ESP_LOGW(TAG, "'%s' - No option set", name); return {}; } auto idx = this->index_.value(); if (idx >= options.size()) { - ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", this->parent_->get_name().c_str(), idx); + ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", name, idx); return {}; } if (this->operation_ == SELECT_OP_SET) { - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGD(TAG, "'%s' - Setting", name); } return idx; } // SELECT_OP_NEXT or SELECT_OP_PREVIOUS - ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", this->parent_->get_name().c_str(), - this->operation_ == SELECT_OP_NEXT ? "next" : "previous", this->cycle_ ? "" : "out"); + ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, this->operation_ == SELECT_OP_NEXT ? "next" : "previous", + this->cycle_ ? "" : "out"); const auto size = options.size(); if (!this->parent_->has_state()) { @@ -105,20 +105,21 @@ optional SelectCall::calculate_target_index_() { void SelectCall::perform() { auto *parent = this->parent_; + const auto *name = parent->get_name().c_str(); if (this->operation_ == SELECT_OP_NONE) { - ESP_LOGW(TAG, "'%s' - SelectCall performed without selecting an operation", parent->get_name().c_str()); + ESP_LOGW(TAG, "'%s' - SelectCall performed without selecting an operation", name); return; } - auto target_index = this->calculate_target_index_(); + auto target_index = this->calculate_target_index_(name); if (!target_index.has_value()) { return; } auto idx = target_index.value(); // All operations use indices, call control() by index to avoid string conversion - ESP_LOGD(TAG, "'%s' - Set selected option to: %s", parent->get_name().c_str(), parent->option_at(idx)); + ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, parent->option_at(idx)); parent->control(idx); } diff --git a/esphome/components/select/select_call.h b/esphome/components/select/select_call.h index dc6bc85014..0b83cb143a 100644 --- a/esphome/components/select/select_call.h +++ b/esphome/components/select/select_call.h @@ -38,7 +38,7 @@ class SelectCall { SelectCall &with_index(size_t index); protected: - __attribute__((always_inline)) inline optional calculate_target_index_(); + __attribute__((always_inline)) inline optional calculate_target_index_(const char *name); Select *const parent_; optional index_; From f86c74ff02f2ad924343e74b48936bc43204c3ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 15:20:50 -0500 Subject: [PATCH 280/394] preen --- esphome/components/select/select_call.cpp | 8 +++----- esphome/components/select/select_call.h | 1 - 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index ae1e6b2f39..37462b67ec 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -13,7 +13,7 @@ SelectCall &SelectCall::set_option(const std::string &option) { SelectCall &SelectCall::set_option(const char *option) { return with_operation(SELECT_OP_SET).with_option(option); } -SelectCall &SelectCall::set_index(size_t index) { return with_operation(SELECT_OP_SET_INDEX).with_index(index); } +SelectCall &SelectCall::set_index(size_t index) { return with_operation(SELECT_OP_SET).with_index(index); } SelectCall &SelectCall::select_next(bool cycle) { return with_operation(SELECT_OP_NEXT).with_cycle(cycle); } @@ -61,7 +61,8 @@ optional SelectCall::calculate_target_index_(const char *name) { return options.size() - 1; } - if (this->operation_ == SELECT_OP_SET || this->operation_ == SELECT_OP_SET_INDEX) { + if (this->operation_ == SELECT_OP_SET) { + ESP_LOGD(TAG, "'%s' - Setting", name); if (!this->index_.has_value()) { ESP_LOGW(TAG, "'%s' - No option set", name); return {}; @@ -71,9 +72,6 @@ optional SelectCall::calculate_target_index_(const char *name) { ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", name, idx); return {}; } - if (this->operation_ == SELECT_OP_SET) { - ESP_LOGD(TAG, "'%s' - Setting", name); - } return idx; } diff --git a/esphome/components/select/select_call.h b/esphome/components/select/select_call.h index 0b83cb143a..eae7d3de1d 100644 --- a/esphome/components/select/select_call.h +++ b/esphome/components/select/select_call.h @@ -10,7 +10,6 @@ class Select; enum SelectOperation { SELECT_OP_NONE, SELECT_OP_SET, - SELECT_OP_SET_INDEX, SELECT_OP_NEXT, SELECT_OP_PREVIOUS, SELECT_OP_FIRST, From 394d50a3282f815679f47c9a7151762879874651 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 15:24:02 -0500 Subject: [PATCH 281/394] esphom prefers this-> --- esphome/components/select/select_call.cpp | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index 37462b67ec..72cbbdc0ef 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -8,20 +8,24 @@ namespace select { static const char *const TAG = "select"; SelectCall &SelectCall::set_option(const std::string &option) { - return with_operation(SELECT_OP_SET).with_option(option); + return this->with_operation(SELECT_OP_SET).with_option(option); } -SelectCall &SelectCall::set_option(const char *option) { return with_operation(SELECT_OP_SET).with_option(option); } +SelectCall &SelectCall::set_option(const char *option) { + return this->with_operation(SELECT_OP_SET).with_option(option); +} -SelectCall &SelectCall::set_index(size_t index) { return with_operation(SELECT_OP_SET).with_index(index); } +SelectCall &SelectCall::set_index(size_t index) { return this->with_operation(SELECT_OP_SET).with_index(index); } -SelectCall &SelectCall::select_next(bool cycle) { return with_operation(SELECT_OP_NEXT).with_cycle(cycle); } +SelectCall &SelectCall::select_next(bool cycle) { return this->with_operation(SELECT_OP_NEXT).with_cycle(cycle); } -SelectCall &SelectCall::select_previous(bool cycle) { return with_operation(SELECT_OP_PREVIOUS).with_cycle(cycle); } +SelectCall &SelectCall::select_previous(bool cycle) { + return this->with_operation(SELECT_OP_PREVIOUS).with_cycle(cycle); +} -SelectCall &SelectCall::select_first() { return with_operation(SELECT_OP_FIRST); } +SelectCall &SelectCall::select_first() { return this->with_operation(SELECT_OP_FIRST); } -SelectCall &SelectCall::select_last() { return with_operation(SELECT_OP_LAST); } +SelectCall &SelectCall::select_last() { return this->with_operation(SELECT_OP_LAST); } SelectCall &SelectCall::with_operation(SelectOperation operation) { this->operation_ = operation; From 774cdd33bc35c5e787d8971ebbd8bed09c3e9a7a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 15:27:44 -0500 Subject: [PATCH 282/394] cleaner --- esphome/components/select/select_call.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index 72cbbdc0ef..e1cabbd3d4 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -46,7 +46,12 @@ SelectCall &SelectCall::with_option(const char *option) { } SelectCall &SelectCall::with_index(size_t index) { - this->index_ = index; + if (index >= this->parent_->size()) { + ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", this->parent_->get_name().c_str(), index); + this->index_ = {}; // Store nullopt for invalid index + } else { + this->index_ = index; + } return *this; } @@ -71,12 +76,7 @@ optional SelectCall::calculate_target_index_(const char *name) { ESP_LOGW(TAG, "'%s' - No option set", name); return {}; } - auto idx = this->index_.value(); - if (idx >= options.size()) { - ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", name, idx); - return {}; - } - return idx; + return this->index_.value(); } // SELECT_OP_NEXT or SELECT_OP_PREVIOUS @@ -114,6 +114,7 @@ void SelectCall::perform() { return; } + // Calculate target index (with_index() and with_option() already validate bounds/existence) auto target_index = this->calculate_target_index_(name); if (!target_index.has_value()) { return; From 10b9ec32a8a114551f4968979f9eec0e61de0da9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 15:33:07 -0500 Subject: [PATCH 283/394] preen --- esphome/components/select/select_call.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index e1cabbd3d4..4e3dbf8cf8 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -7,15 +7,11 @@ namespace select { static const char *const TAG = "select"; -SelectCall &SelectCall::set_option(const std::string &option) { - return this->with_operation(SELECT_OP_SET).with_option(option); -} +SelectCall &SelectCall::set_option(const std::string &option) { return this->with_option(option); } -SelectCall &SelectCall::set_option(const char *option) { - return this->with_operation(SELECT_OP_SET).with_option(option); -} +SelectCall &SelectCall::set_option(const char *option) { return this->with_option(option); } -SelectCall &SelectCall::set_index(size_t index) { return this->with_operation(SELECT_OP_SET).with_index(index); } +SelectCall &SelectCall::set_index(size_t index) { return this->with_index(index); } SelectCall &SelectCall::select_next(bool cycle) { return this->with_operation(SELECT_OP_NEXT).with_cycle(cycle); } @@ -40,12 +36,14 @@ SelectCall &SelectCall::with_cycle(bool cycle) { SelectCall &SelectCall::with_option(const std::string &option) { return this->with_option(option.c_str()); } SelectCall &SelectCall::with_option(const char *option) { + this->operation_ = SELECT_OP_SET; // Find the option index - this validates the option exists this->index_ = this->parent_->index_of(option); return *this; } SelectCall &SelectCall::with_index(size_t index) { + this->operation_ = SELECT_OP_SET; if (index >= this->parent_->size()) { ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", this->parent_->get_name().c_str(), index); this->index_ = {}; // Store nullopt for invalid index From 2a73fd3fd6f545728c05c75607b7eedb85f01553 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 15:38:40 -0500 Subject: [PATCH 284/394] esp8266 --- esphome/components/select/select_call.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index 4e3dbf8cf8..154e125eaa 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -78,8 +78,9 @@ optional SelectCall::calculate_target_index_(const char *name) { } // SELECT_OP_NEXT or SELECT_OP_PREVIOUS - ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, this->operation_ == SELECT_OP_NEXT ? "next" : "previous", - this->cycle_ ? "" : "out"); + ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, + this->operation_ == SELECT_OP_NEXT ? LOG_STR_LITERAL("next") : LOG_STR_LITERAL("previous"), + this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); const auto size = options.size(); if (!this->parent_->has_state()) { From 19e1427d92ebc98f3f9d48945a4d2087a04d4c4d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 15:40:10 -0500 Subject: [PATCH 285/394] wip --- esphome/components/select/select_call.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index 154e125eaa..aa7559e24e 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -88,17 +88,17 @@ optional SelectCall::calculate_target_index_(const char *name) { } // Use cached active_index_ instead of index_of() lookup - const auto index = this->parent_->active_index_; + const auto active_index = this->parent_->active_index_; if (this->cycle_) { - return (size + index + (this->operation_ == SELECT_OP_NEXT ? +1 : -1)) % size; + return (size + active_index + (this->operation_ == SELECT_OP_NEXT ? +1 : -1)) % size; } - if (this->operation_ == SELECT_OP_PREVIOUS && index > 0) { - return index - 1; + if (this->operation_ == SELECT_OP_PREVIOUS && active_index > 0) { + return active_index - 1; } - if (this->operation_ == SELECT_OP_NEXT && index < size - 1) { - return index + 1; + if (this->operation_ == SELECT_OP_NEXT && active_index < size - 1) { + return active_index + 1; } return {}; // Can't navigate further without cycling From fd64585f99fe47e749606823231020915d6fa75e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 16:50:06 -0500 Subject: [PATCH 286/394] Bump github/codeql-action from 4.31.0 to 4.31.2 (#11626) 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 6b940eed8a..ab938b3436 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@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 + uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 + uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 with: category: "/language:${{matrix.language}}" From 59736f25e9f7bc15971ecf39c2d212a9a8835dc3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 17:43:45 -0500 Subject: [PATCH 287/394] wip --- esphome/components/esp32_ble/ble.cpp | 89 +++++++++++++++++----------- esphome/components/esp32_ble/ble.h | 3 + 2 files changed, 56 insertions(+), 36 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 5bbd5fe9ed..b881211a26 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -335,6 +335,58 @@ bool ESP32BLE::ble_dismantle_() { return true; } +#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT +inline void ESP32BLE::dispatch_gap_event_(esp_gap_ble_cb_event_t gap_event, BLEEvent *ble_event) { + // Determine which union member to use based on event type. + // All event structures are properly laid out in memory per ESP-IDF. + // The reinterpret_cast operations are safe because: + // 1. Structure sizes match ESP-IDF expectations (verified by static_assert in ble_event.h) + // 2. Status fields are at offset 0 (verified by static_assert in ble_event.h) + // 3. The struct already contains our copy of the data (copied in BLEEvent constructor) + esp_ble_gap_cb_param_t *param; + + switch (gap_event) { + // Scan complete events - all have same structure with just status + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + param = reinterpret_cast(&ble_event->event_.gap.scan_complete); + break; + + // Advertising complete events - all have same structure with just status + case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: + case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: + param = reinterpret_cast(&ble_event->event_.gap.adv_complete); + break; + + // RSSI complete event + case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + param = reinterpret_cast(&ble_event->event_.gap.read_rssi_complete); + break; + + // Security events + case ESP_GAP_BLE_AUTH_CMPL_EVT: + case ESP_GAP_BLE_SEC_REQ_EVT: + case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: + case ESP_GAP_BLE_PASSKEY_REQ_EVT: + case ESP_GAP_BLE_NC_REQ_EVT: + param = reinterpret_cast(&ble_event->event_.gap.security); + break; + + default: + return; // Shouldn't happen - all cases covered by loop() switch + } + + // Dispatch to all registered handlers + for (auto *gap_handler : this->gap_event_handlers_) { + gap_handler->gap_event_handler(gap_event, param); + } +} +#endif + void ESP32BLE::loop() { switch (this->state_) { case BLE_COMPONENT_STATE_OFF: @@ -417,46 +469,14 @@ void ESP32BLE::loop() { case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: - // All three scan complete events have the same structure with just status - // The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe - // This is verified at compile-time by static_assert checks in ble_event.h - // The struct already contains our copy of the status (copied in BLEEvent constructor) - ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); -#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT - for (auto *gap_handler : this->gap_event_handlers_) { - gap_handler->gap_event_handler( - gap_event, reinterpret_cast(&ble_event->event_.gap.scan_complete)); - } -#endif - break; - // Advertising complete events case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: - // All advertising complete events have the same structure with just status - ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); -#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT - for (auto *gap_handler : this->gap_event_handlers_) { - gap_handler->gap_event_handler( - gap_event, reinterpret_cast(&ble_event->event_.gap.adv_complete)); - } -#endif - break; - // RSSI complete event case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: - ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); -#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT - for (auto *gap_handler : this->gap_event_handlers_) { - gap_handler->gap_event_handler( - gap_event, reinterpret_cast(&ble_event->event_.gap.read_rssi_complete)); - } -#endif - break; - // Security events case ESP_GAP_BLE_AUTH_CMPL_EVT: case ESP_GAP_BLE_SEC_REQ_EVT: @@ -465,10 +485,7 @@ void ESP32BLE::loop() { case ESP_GAP_BLE_NC_REQ_EVT: ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT - for (auto *gap_handler : this->gap_event_handlers_) { - gap_handler->gap_event_handler( - gap_event, reinterpret_cast(&ble_event->event_.gap.security)); - } + this->dispatch_gap_event_(gap_event, ble_event); #endif break; diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index dc973f0e82..8c2954d571 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -161,6 +161,9 @@ class ESP32BLE : public Component { #ifdef USE_ESP32_BLE_ADVERTISING void advertising_init_(); #endif +#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT + void dispatch_gap_event_(esp_gap_ble_cb_event_t gap_event, BLEEvent *ble_event); +#endif private: template friend void enqueue_ble_event(Args... args); From 1905bbd8984fb727cd17cc945c53192089e60ed9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 17:49:20 -0500 Subject: [PATCH 288/394] dry --- esphome/components/esp32_ble/ble.cpp | 80 ++++++++++++---------------- 1 file changed, 35 insertions(+), 45 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index b881211a26..9d7b471be8 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -31,6 +31,26 @@ namespace esphome::esp32_ble { static const char *const TAG = "esp32_ble"; +// GAP event groups for deduplication across gap_event_handler and dispatch_gap_event_ +#define GAP_SCAN_COMPLETE_EVENTS \ + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: \ + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: \ + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT + +#define GAP_ADV_COMPLETE_EVENTS \ + case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: \ + case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: \ + case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: \ + case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: \ + case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT + +#define GAP_SECURITY_EVENTS \ + case ESP_GAP_BLE_AUTH_CMPL_EVT: \ + case ESP_GAP_BLE_SEC_REQ_EVT: \ + case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: \ + case ESP_GAP_BLE_PASSKEY_REQ_EVT: \ + case ESP_GAP_BLE_NC_REQ_EVT + void ESP32BLE::setup() { global_ble = this; if (!ble_pre_setup_()) { @@ -346,21 +366,15 @@ inline void ESP32BLE::dispatch_gap_event_(esp_gap_ble_cb_event_t gap_event, BLEE esp_ble_gap_cb_param_t *param; switch (gap_event) { - // Scan complete events - all have same structure with just status - case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: - case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: - case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: - param = reinterpret_cast(&ble_event->event_.gap.scan_complete); - break; + // Scan complete events - all have same structure with just status + GAP_SCAN_COMPLETE_EVENTS: + param = reinterpret_cast(&ble_event->event_.gap.scan_complete); + break; - // Advertising complete events - all have same structure with just status - case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: - case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: - case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: - case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: - case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: - param = reinterpret_cast(&ble_event->event_.gap.adv_complete); - break; + // Advertising complete events - all have same structure with just status + GAP_ADV_COMPLETE_EVENTS: + param = reinterpret_cast(&ble_event->event_.gap.adv_complete); + break; // RSSI complete event case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: @@ -368,11 +382,7 @@ inline void ESP32BLE::dispatch_gap_event_(esp_gap_ble_cb_event_t gap_event, BLEE break; // Security events - case ESP_GAP_BLE_AUTH_CMPL_EVT: - case ESP_GAP_BLE_SEC_REQ_EVT: - case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: - case ESP_GAP_BLE_PASSKEY_REQ_EVT: - case ESP_GAP_BLE_NC_REQ_EVT: + GAP_SECURITY_EVENTS: param = reinterpret_cast(&ble_event->event_.gap.security); break; @@ -466,23 +476,13 @@ void ESP32BLE::loop() { break; // Scan complete events - case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: - case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: - case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + GAP_SCAN_COMPLETE_EVENTS: // Advertising complete events - case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: - case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: - case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: - case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: - case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: + GAP_ADV_COMPLETE_EVENTS: // RSSI complete event case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: // Security events - case ESP_GAP_BLE_AUTH_CMPL_EVT: - case ESP_GAP_BLE_SEC_REQ_EVT: - case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: - case ESP_GAP_BLE_PASSKEY_REQ_EVT: - case ESP_GAP_BLE_NC_REQ_EVT: + GAP_SECURITY_EVENTS: ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT this->dispatch_gap_event_(gap_event, ble_event); @@ -564,23 +564,13 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa // Queue GAP events that components need to handle // Scanning events - used by esp32_ble_tracker case ESP_GAP_BLE_SCAN_RESULT_EVT: - case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: - case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: - case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + GAP_SCAN_COMPLETE_EVENTS: // Advertising events - used by esp32_ble_beacon and esp32_ble server - case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: - case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: - case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: - case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: - case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: + GAP_ADV_COMPLETE_EVENTS: // Connection events - used by ble_client case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: // Security events - used by ble_client and bluetooth_proxy - case ESP_GAP_BLE_AUTH_CMPL_EVT: - case ESP_GAP_BLE_SEC_REQ_EVT: - case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: - case ESP_GAP_BLE_PASSKEY_REQ_EVT: - case ESP_GAP_BLE_NC_REQ_EVT: + GAP_SECURITY_EVENTS: enqueue_ble_event(event, param); return; From 1925cd03795743947d852584d066df66e1d5e214 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 17:53:34 -0500 Subject: [PATCH 289/394] dry --- esphome/components/esp32_ble/ble.cpp | 74 ++++++++++++---------------- esphome/components/esp32_ble/ble.h | 3 -- 2 files changed, 31 insertions(+), 46 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 9d7b471be8..117f489777 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -355,48 +355,6 @@ bool ESP32BLE::ble_dismantle_() { return true; } -#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT -inline void ESP32BLE::dispatch_gap_event_(esp_gap_ble_cb_event_t gap_event, BLEEvent *ble_event) { - // Determine which union member to use based on event type. - // All event structures are properly laid out in memory per ESP-IDF. - // The reinterpret_cast operations are safe because: - // 1. Structure sizes match ESP-IDF expectations (verified by static_assert in ble_event.h) - // 2. Status fields are at offset 0 (verified by static_assert in ble_event.h) - // 3. The struct already contains our copy of the data (copied in BLEEvent constructor) - esp_ble_gap_cb_param_t *param; - - switch (gap_event) { - // Scan complete events - all have same structure with just status - GAP_SCAN_COMPLETE_EVENTS: - param = reinterpret_cast(&ble_event->event_.gap.scan_complete); - break; - - // Advertising complete events - all have same structure with just status - GAP_ADV_COMPLETE_EVENTS: - param = reinterpret_cast(&ble_event->event_.gap.adv_complete); - break; - - // RSSI complete event - case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: - param = reinterpret_cast(&ble_event->event_.gap.read_rssi_complete); - break; - - // Security events - GAP_SECURITY_EVENTS: - param = reinterpret_cast(&ble_event->event_.gap.security); - break; - - default: - return; // Shouldn't happen - all cases covered by loop() switch - } - - // Dispatch to all registered handlers - for (auto *gap_handler : this->gap_event_handlers_) { - gap_handler->gap_event_handler(gap_event, param); - } -} -#endif - void ESP32BLE::loop() { switch (this->state_) { case BLE_COMPONENT_STATE_OFF: @@ -485,7 +443,37 @@ void ESP32BLE::loop() { GAP_SECURITY_EVENTS: ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT - this->dispatch_gap_event_(gap_event, ble_event); + { + // Determine which union member to use based on event type. + // All event structures are properly laid out in memory per ESP-IDF. + // The reinterpret_cast operations are safe because: + // 1. Structure sizes match ESP-IDF expectations (verified by static_assert in ble_event.h) + // 2. Status fields are at offset 0 (verified by static_assert in ble_event.h) + // 3. The struct already contains our copy of the data (copied in BLEEvent constructor) + esp_ble_gap_cb_param_t *param; + // clang-format off + switch (gap_event) { + GAP_SCAN_COMPLETE_EVENTS: + param = reinterpret_cast(&ble_event->event_.gap.scan_complete); + break; + GAP_ADV_COMPLETE_EVENTS: + param = reinterpret_cast(&ble_event->event_.gap.adv_complete); + break; + case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + param = reinterpret_cast(&ble_event->event_.gap.read_rssi_complete); + break; + GAP_SECURITY_EVENTS: + param = reinterpret_cast(&ble_event->event_.gap.security); + break; + default: + break; + } + // clang-format on + // Dispatch to all registered handlers + for (auto *gap_handler : this->gap_event_handlers_) { + gap_handler->gap_event_handler(gap_event, param); + } + } #endif break; diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 8c2954d571..dc973f0e82 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -161,9 +161,6 @@ class ESP32BLE : public Component { #ifdef USE_ESP32_BLE_ADVERTISING void advertising_init_(); #endif -#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT - void dispatch_gap_event_(esp_gap_ble_cb_event_t gap_event, BLEEvent *ble_event); -#endif private: template friend void enqueue_ble_event(Args... args); From d848cc33d7fd4d197f9697590234099c3f6dd5ba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 17:54:35 -0500 Subject: [PATCH 290/394] dry --- esphome/components/esp32_ble/ble.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 117f489777..69e317ff6d 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -444,27 +444,30 @@ void ESP32BLE::loop() { ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT { - // Determine which union member to use based on event type. - // All event structures are properly laid out in memory per ESP-IDF. - // The reinterpret_cast operations are safe because: - // 1. Structure sizes match ESP-IDF expectations (verified by static_assert in ble_event.h) - // 2. Status fields are at offset 0 (verified by static_assert in ble_event.h) - // 3. The struct already contains our copy of the data (copied in BLEEvent constructor) esp_ble_gap_cb_param_t *param; // clang-format off switch (gap_event) { + // All three scan complete events have the same structure with just status + // The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe + // This is verified at compile-time by static_assert checks in ble_event.h + // The struct already contains our copy of the status (copied in BLEEvent constructor) GAP_SCAN_COMPLETE_EVENTS: param = reinterpret_cast(&ble_event->event_.gap.scan_complete); break; + + // All advertising complete events have the same structure with just status GAP_ADV_COMPLETE_EVENTS: param = reinterpret_cast(&ble_event->event_.gap.adv_complete); break; + case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: param = reinterpret_cast(&ble_event->event_.gap.read_rssi_complete); break; + GAP_SECURITY_EVENTS: param = reinterpret_cast(&ble_event->event_.gap.security); break; + default: break; } From 210320b8ccb9b4f628e4ba325caa451cff2c4923 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 18:43:17 -0500 Subject: [PATCH 291/394] simplify --- esphome/components/climate/climate.cpp | 126 ++++++++++++-------- esphome/components/climate/climate.h | 31 +++-- esphome/components/climate/climate_traits.h | 14 ++- 3 files changed, 107 insertions(+), 64 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 64f43ffd80..275db1d423 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -50,21 +50,21 @@ void ClimateCall::perform() { const LogString *mode_s = climate_mode_to_string(*this->mode_); ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(mode_s)); } - if (this->custom_fan_mode_.has_value()) { + if (this->custom_fan_mode_ != nullptr) { this->fan_mode_.reset(); - ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_.value().c_str()); + ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_); } if (this->fan_mode_.has_value()) { - this->custom_fan_mode_.reset(); + this->custom_fan_mode_ = nullptr; const LogString *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_); ESP_LOGD(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s)); } - if (this->custom_preset_.has_value()) { + if (this->custom_preset_ != nullptr) { this->preset_.reset(); - ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_.value().c_str()); + ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_); } if (this->preset_.has_value()) { - this->custom_preset_.reset(); + this->custom_preset_ = nullptr; const LogString *preset_s = climate_preset_to_string(*this->preset_); ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(preset_s)); } @@ -96,11 +96,10 @@ void ClimateCall::validate_() { this->mode_.reset(); } } - if (this->custom_fan_mode_.has_value()) { - auto custom_fan_mode = *this->custom_fan_mode_; - if (!traits.supports_custom_fan_mode(custom_fan_mode)) { - ESP_LOGW(TAG, " Fan Mode %s not supported", custom_fan_mode.c_str()); - this->custom_fan_mode_.reset(); + if (this->custom_fan_mode_ != nullptr) { + if (!traits.supports_custom_fan_mode(this->custom_fan_mode_)) { + ESP_LOGW(TAG, " Fan Mode %s not supported", this->custom_fan_mode_); + this->custom_fan_mode_ = nullptr; } } else if (this->fan_mode_.has_value()) { auto fan_mode = *this->fan_mode_; @@ -109,11 +108,10 @@ void ClimateCall::validate_() { this->fan_mode_.reset(); } } - if (this->custom_preset_.has_value()) { - auto custom_preset = *this->custom_preset_; - if (!traits.supports_custom_preset(custom_preset)) { - ESP_LOGW(TAG, " Preset %s not supported", custom_preset.c_str()); - this->custom_preset_.reset(); + if (this->custom_preset_ != nullptr) { + if (!traits.supports_custom_preset(this->custom_preset_)) { + ESP_LOGW(TAG, " Preset %s not supported", this->custom_preset_); + this->custom_preset_ = nullptr; } } else if (this->preset_.has_value()) { auto preset = *this->preset_; @@ -186,26 +184,33 @@ ClimateCall &ClimateCall::set_mode(const std::string &mode) { ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) { this->fan_mode_ = fan_mode; - this->custom_fan_mode_.reset(); + this->custom_fan_mode_ = nullptr; return *this; } -ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { +ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode) { + // Check if it's a standard enum mode first for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) { - if (str_equals_case_insensitive(fan_mode, mode_entry.str)) { + if (str_equals_case_insensitive(custom_fan_mode, mode_entry.str)) { this->set_fan_mode(static_cast(mode_entry.value)); return *this; } } - if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) { - this->custom_fan_mode_ = fan_mode; - this->fan_mode_.reset(); - } else { - ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str()); + // Find the matching pointer from traits + const auto &supported = this->parent_->get_traits().get_supported_custom_fan_modes(); + for (const char *mode : supported) { + if (strcmp(mode, custom_fan_mode) == 0) { + this->custom_fan_mode_ = mode; + this->fan_mode_.reset(); + return *this; + } } + ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), custom_fan_mode); return *this; } +ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { return this->set_fan_mode(fan_mode.c_str()); } + ClimateCall &ClimateCall::set_fan_mode(optional fan_mode) { if (fan_mode.has_value()) { this->set_fan_mode(fan_mode.value()); @@ -215,26 +220,33 @@ ClimateCall &ClimateCall::set_fan_mode(optional fan_mode) { ClimateCall &ClimateCall::set_preset(ClimatePreset preset) { this->preset_ = preset; - this->custom_preset_.reset(); + this->custom_preset_ = nullptr; return *this; } -ClimateCall &ClimateCall::set_preset(const std::string &preset) { +ClimateCall &ClimateCall::set_preset(const char *custom_preset) { + // Check if it's a standard enum preset first for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) { - if (str_equals_case_insensitive(preset, preset_entry.str)) { + if (str_equals_case_insensitive(custom_preset, preset_entry.str)) { this->set_preset(static_cast(preset_entry.value)); return *this; } } - if (this->parent_->get_traits().supports_custom_preset(preset)) { - this->custom_preset_ = preset; - this->preset_.reset(); - } else { - ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str()); + // Find the matching pointer from traits + const auto &supported = this->parent_->get_traits().get_supported_custom_presets(); + for (const char *preset : supported) { + if (strcmp(preset, custom_preset) == 0) { + this->custom_preset_ = preset; + this->preset_.reset(); + return *this; + } } + ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), custom_preset); return *this; } +ClimateCall &ClimateCall::set_preset(const std::string &preset) { return this->set_preset(preset.c_str()); } + ClimateCall &ClimateCall::set_preset(optional preset) { if (preset.has_value()) { this->set_preset(preset.value()); @@ -287,8 +299,22 @@ const optional &ClimateCall::get_mode() const { return this->mode_; const optional &ClimateCall::get_fan_mode() const { return this->fan_mode_; } const optional &ClimateCall::get_swing_mode() const { return this->swing_mode_; } const optional &ClimateCall::get_preset() const { return this->preset_; } -const optional &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } -const optional &ClimateCall::get_custom_preset() const { return this->custom_preset_; } +const char *ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } +const char *ClimateCall::get_custom_preset() const { return this->custom_preset_; } + +optional ClimateCall::get_custom_fan_mode_optional() const { + if (this->custom_fan_mode_ != nullptr) { + return std::string(this->custom_fan_mode_); + } + return {}; +} + +optional ClimateCall::get_custom_preset_optional() const { + if (this->custom_preset_ != nullptr) { + return std::string(this->custom_preset_); + } + return {}; +} ClimateCall &ClimateCall::set_target_temperature_high(optional target_temperature_high) { this->target_temperature_high_ = target_temperature_high; @@ -317,13 +343,13 @@ ClimateCall &ClimateCall::set_mode(optional mode) { ClimateCall &ClimateCall::set_fan_mode(optional fan_mode) { this->fan_mode_ = fan_mode; - this->custom_fan_mode_.reset(); + this->custom_fan_mode_ = nullptr; return *this; } ClimateCall &ClimateCall::set_preset(optional preset) { this->preset_ = preset; - this->custom_preset_.reset(); + this->custom_preset_ = nullptr; return *this; } @@ -382,13 +408,13 @@ void Climate::save_state_() { state.uses_custom_fan_mode = false; state.fan_mode = this->fan_mode.value(); } - if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) { + if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode != nullptr) { state.uses_custom_fan_mode = true; const auto &supported = traits.get_supported_custom_fan_modes(); // std::vector maintains insertion order size_t i = 0; for (const char *mode : supported) { - if (strcmp(mode, custom_fan_mode.value().c_str()) == 0) { + if (strcmp(mode, custom_fan_mode) == 0) { state.custom_fan_mode = i; break; } @@ -399,13 +425,13 @@ void Climate::save_state_() { state.uses_custom_preset = false; state.preset = this->preset.value(); } - if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) { + if (!traits.get_supported_custom_presets().empty() && custom_preset != nullptr) { state.uses_custom_preset = true; const auto &supported = traits.get_supported_custom_presets(); // std::vector maintains insertion order size_t i = 0; for (const char *preset : supported) { - if (strcmp(preset, custom_preset.value().c_str()) == 0) { + if (strcmp(preset, custom_preset) == 0) { state.custom_preset = i; break; } @@ -430,14 +456,14 @@ void Climate::publish_state() { if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) { ESP_LOGD(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value()))); } - if (!traits.get_supported_custom_fan_modes().empty() && this->custom_fan_mode.has_value()) { - ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode.value().c_str()); + if (!traits.get_supported_custom_fan_modes().empty() && this->custom_fan_mode != nullptr) { + ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode); } if (traits.get_supports_presets() && this->preset.has_value()) { ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value()))); } - if (!traits.get_supported_custom_presets().empty() && this->custom_preset.has_value()) { - ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset.value().c_str()); + if (!traits.get_supported_custom_presets().empty() && this->custom_preset != nullptr) { + ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset); } if (traits.get_supports_swing_modes()) { ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode))); @@ -527,7 +553,7 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { if (this->uses_custom_fan_mode) { if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { call.fan_mode_.reset(); - call.custom_fan_mode_ = std::string(traits.get_supported_custom_fan_modes()[this->custom_fan_mode]); + call.custom_fan_mode_ = traits.get_supported_custom_fan_modes()[this->custom_fan_mode]; } } else if (traits.supports_fan_mode(this->fan_mode)) { call.set_fan_mode(this->fan_mode); @@ -535,7 +561,7 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { if (this->uses_custom_preset) { if (this->custom_preset < traits.get_supported_custom_presets().size()) { call.preset_.reset(); - call.custom_preset_ = std::string(traits.get_supported_custom_presets()[this->custom_preset]); + call.custom_preset_ = traits.get_supported_custom_presets()[this->custom_preset]; } } else if (traits.supports_preset(this->preset)) { call.set_preset(this->preset); @@ -562,20 +588,20 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { if (this->uses_custom_fan_mode) { if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { climate->fan_mode.reset(); - climate->custom_fan_mode = std::string(traits.get_supported_custom_fan_modes()[this->custom_fan_mode]); + climate->custom_fan_mode = traits.get_supported_custom_fan_modes()[this->custom_fan_mode]; } } else if (traits.supports_fan_mode(this->fan_mode)) { climate->fan_mode = this->fan_mode; - climate->custom_fan_mode.reset(); + climate->custom_fan_mode = nullptr; } if (this->uses_custom_preset) { if (this->custom_preset < traits.get_supported_custom_presets().size()) { climate->preset.reset(); - climate->custom_preset = std::string(traits.get_supported_custom_presets()[this->custom_preset]); + climate->custom_preset = traits.get_supported_custom_presets()[this->custom_preset]; } } else if (traits.supports_preset(this->preset)) { climate->preset = this->preset; - climate->custom_preset.reset(); + climate->custom_preset = nullptr; } if (traits.supports_swing_mode(this->swing_mode)) { climate->swing_mode = this->swing_mode; diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 0c3e3ebe16..49ea2a47a8 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -74,9 +74,13 @@ class ClimateCall { /// Set the fan mode of the climate device. ClimateCall &set_fan_mode(optional fan_mode); /// Set the fan mode of the climate device based on a string. - ClimateCall &set_fan_mode(const std::string &fan_mode); + __attribute__((deprecated("Use set_fan_mode(const char*) instead"))) ClimateCall &set_fan_mode( + const std::string &fan_mode); /// Set the fan mode of the climate device based on a string. - ClimateCall &set_fan_mode(optional fan_mode); + __attribute__((deprecated("Use set_fan_mode(const char*) instead"))) ClimateCall &set_fan_mode( + optional fan_mode); + /// Set the custom fan mode of the climate device. + ClimateCall &set_fan_mode(const char *custom_fan_mode); /// Set the swing mode of the climate device. ClimateCall &set_swing_mode(ClimateSwingMode swing_mode); /// Set the swing mode of the climate device. @@ -88,9 +92,12 @@ class ClimateCall { /// Set the preset of the climate device. ClimateCall &set_preset(optional preset); /// Set the preset of the climate device based on a string. - ClimateCall &set_preset(const std::string &preset); + __attribute__((deprecated("Use set_preset(const char*) instead"))) ClimateCall &set_preset(const std::string &preset); /// Set the preset of the climate device based on a string. - ClimateCall &set_preset(optional preset); + __attribute__((deprecated("Use set_preset(const char*) instead"))) ClimateCall &set_preset( + optional preset); + /// Set the custom preset of the climate device. + ClimateCall &set_preset(const char *custom_preset); void perform(); @@ -103,8 +110,12 @@ class ClimateCall { const optional &get_fan_mode() const; const optional &get_swing_mode() const; const optional &get_preset() const; - const optional &get_custom_fan_mode() const; - const optional &get_custom_preset() const; + const char *get_custom_fan_mode() const; + const char *get_custom_preset() const; + /// @deprecated Use get_custom_fan_mode() (returns const char*) instead (since 2025.11.0) + optional get_custom_fan_mode_optional() const; + /// @deprecated Use get_custom_preset() (returns const char*) instead (since 2025.11.0) + optional get_custom_preset_optional() const; protected: void validate_(); @@ -118,8 +129,8 @@ class ClimateCall { optional fan_mode_; optional swing_mode_; optional preset_; - optional custom_fan_mode_; - optional custom_preset_; + const char *custom_fan_mode_{nullptr}; + const char *custom_preset_{nullptr}; }; /// Struct used to save the state of the climate device in restore memory. @@ -239,10 +250,10 @@ class Climate : public EntityBase { optional preset; /// The active custom fan mode of the climate device. - optional custom_fan_mode; + const char *custom_fan_mode{nullptr}; /// The active custom preset mode of the climate device. - optional custom_preset; + const char *custom_preset{nullptr}; /// The active mode of the climate device. ClimateMode mode{CLIMATE_MODE_OFF}; diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index f0e0dbe02b..7405918fea 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -135,13 +135,16 @@ class ClimateTraits { this->supported_custom_fan_modes_.assign(modes, modes + N); } const std::vector &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } - bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { + bool supports_custom_fan_mode(const char *custom_fan_mode) const { for (const char *mode : this->supported_custom_fan_modes_) { - if (strcmp(mode, custom_fan_mode.c_str()) == 0) + if (strcmp(mode, custom_fan_mode) == 0) return true; } return false; } + bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { + return this->supports_custom_fan_mode(custom_fan_mode.c_str()); + } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); } @@ -159,13 +162,16 @@ class ClimateTraits { this->supported_custom_presets_.assign(presets, presets + N); } const std::vector &get_supported_custom_presets() const { return this->supported_custom_presets_; } - bool supports_custom_preset(const std::string &custom_preset) const { + bool supports_custom_preset(const char *custom_preset) const { for (const char *preset : this->supported_custom_presets_) { - if (strcmp(preset, custom_preset.c_str()) == 0) + if (strcmp(preset, custom_preset) == 0) return true; } return false; } + bool supports_custom_preset(const std::string &custom_preset) const { + return this->supports_custom_preset(custom_preset.c_str()); + } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); } From c3c1ae8e7f75d721fdcd387090f18b2d655918d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 18:44:28 -0500 Subject: [PATCH 292/394] simplify --- esphome/components/climate/climate.cpp | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 275db1d423..f0c466203f 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -619,18 +619,40 @@ template bool set_alternative(optional &dst, optio return is_changed; } +// Overload for optional + const char* pointer +template bool set_alternative(optional &dst, const char *&alt, const T &src) { + bool is_changed = (alt != nullptr); + alt = nullptr; + if (is_changed || dst != src) { + dst = src; + is_changed = true; + } + return is_changed; +} + +// Overload for const char* pointer + optional +template bool set_alternative(const char *&dst, optional &alt, const char *src) { + bool is_changed = alt.has_value(); + alt.reset(); + if (is_changed || dst != src) { + dst = src; + is_changed = true; + } + return is_changed; +} + bool Climate::set_fan_mode_(ClimateFanMode mode) { return set_alternative(this->fan_mode, this->custom_fan_mode, mode); } bool Climate::set_custom_fan_mode_(const std::string &mode) { - return set_alternative(this->custom_fan_mode, this->fan_mode, mode); + return set_alternative(this->custom_fan_mode, this->fan_mode, mode.c_str()); } bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); } bool Climate::set_custom_preset_(const std::string &preset) { - return set_alternative(this->custom_preset, this->preset, preset); + return set_alternative(this->custom_preset, this->preset, preset.c_str()); } void Climate::dump_traits_(const char *tag) { From 9161d3a758c5e673f9dedb3b1a016c932e2ac1f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 18:48:05 -0500 Subject: [PATCH 293/394] simplify --- esphome/components/climate/climate.cpp | 35 ++++++++++---------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index f0c466203f..756051d6ce 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -609,35 +609,26 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { climate->publish_state(); } -template bool set_alternative(optional &dst, optional &alt, const T1 &src) { - bool is_changed = alt.has_value(); - alt.reset(); - if (is_changed || dst != src) { - dst = src; - is_changed = true; - } - return is_changed; -} +// Generic template to set one value while clearing its alternative (mutual exclusion) +// Handles both optional and const char* types automatically using compile-time type detection +template bool set_alternative(T1 &dst, T2 &alt, T3 src) { + bool is_changed = false; -// Overload for optional + const char* pointer -template bool set_alternative(optional &dst, const char *&alt, const T &src) { - bool is_changed = (alt != nullptr); - alt = nullptr; - if (is_changed || dst != src) { - dst = src; - is_changed = true; + // Clear the alternative based on its type (pointer or optional) + if constexpr (std::is_pointer_v>) { + is_changed = (alt != nullptr); + alt = nullptr; + } else { + is_changed = alt.has_value(); + alt.reset(); } - return is_changed; -} -// Overload for const char* pointer + optional -template bool set_alternative(const char *&dst, optional &alt, const char *src) { - bool is_changed = alt.has_value(); - alt.reset(); + // Set the destination value if (is_changed || dst != src) { dst = src; is_changed = true; } + return is_changed; } From 42e6b4326fc2052ff5f8a2c52f549a16c97dfc4b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 18:51:19 -0500 Subject: [PATCH 294/394] simplify --- esphome/components/climate/climate_traits.h | 22 +++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 7405918fea..1d4d8b6097 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -9,6 +9,16 @@ namespace esphome { namespace climate { +// Lightweight linear search for small vectors (1-20 items) of const char* pointers +// Avoids std::find template overhead +inline bool vector_contains(const std::vector &vec, const char *value) { + for (const char *item : vec) { + if (strcmp(item, value) == 0) + return true; + } + return false; +} + // Type aliases for climate enum bitmasks // These replace std::set to eliminate red-black tree overhead // For contiguous enums starting at 0, DefaultBitPolicy provides 1:1 mapping (enum value = bit position) @@ -136,11 +146,7 @@ class ClimateTraits { } const std::vector &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } bool supports_custom_fan_mode(const char *custom_fan_mode) const { - for (const char *mode : this->supported_custom_fan_modes_) { - if (strcmp(mode, custom_fan_mode) == 0) - return true; - } - return false; + return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode); } bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { return this->supports_custom_fan_mode(custom_fan_mode.c_str()); @@ -163,11 +169,7 @@ class ClimateTraits { } const std::vector &get_supported_custom_presets() const { return this->supported_custom_presets_; } bool supports_custom_preset(const char *custom_preset) const { - for (const char *preset : this->supported_custom_presets_) { - if (strcmp(preset, custom_preset) == 0) - return true; - } - return false; + return vector_contains(this->supported_custom_presets_, custom_preset); } bool supports_custom_preset(const std::string &custom_preset) const { return this->supports_custom_preset(custom_preset.c_str()); From 4d39e15920d9a85a6a7d8f23e80ab1922336e36b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 18:53:13 -0500 Subject: [PATCH 295/394] simplify --- esphome/components/climate/climate.cpp | 13 ++++++------- esphome/components/climate/climate_traits.h | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 756051d6ce..dc83189692 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -197,13 +197,12 @@ ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode) { } } // Find the matching pointer from traits - const auto &supported = this->parent_->get_traits().get_supported_custom_fan_modes(); - for (const char *mode : supported) { - if (strcmp(mode, custom_fan_mode) == 0) { - this->custom_fan_mode_ = mode; - this->fan_mode_.reset(); - return *this; - } + auto traits = this->parent_->get_traits(); + const char *mode_ptr = traits.find_custom_fan_mode(custom_fan_mode); + if (mode_ptr != nullptr) { + this->custom_fan_mode_ = mode_ptr; + this->fan_mode_.reset(); + return *this; } ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), custom_fan_mode); return *this; diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 1d4d8b6097..e5171867d5 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -151,6 +151,14 @@ class ClimateTraits { bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { return this->supports_custom_fan_mode(custom_fan_mode.c_str()); } + /// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found + const char *find_custom_fan_mode(const char *custom_fan_mode) const { + for (const char *mode : this->supported_custom_fan_modes_) { + if (strcmp(mode, custom_fan_mode) == 0) + return mode; + } + return nullptr; + } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); } @@ -174,6 +182,14 @@ class ClimateTraits { bool supports_custom_preset(const std::string &custom_preset) const { return this->supports_custom_preset(custom_preset.c_str()); } + /// Find and return the matching custom preset pointer from supported presets, or nullptr if not found + const char *find_custom_preset(const char *custom_preset) const { + for (const char *preset : this->supported_custom_presets_) { + if (strcmp(preset, custom_preset) == 0) + return preset; + } + return nullptr; + } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); } From 6b2a85541d7e22a56e0f74953c212b4e5c354db9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 18:55:06 -0500 Subject: [PATCH 296/394] simplify --- esphome/components/api/api_connection.cpp | 4 ++-- esphome/components/climate/climate.cpp | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 382c4acc16..8730828994 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -699,11 +699,11 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { if (msg.has_fan_mode) call.set_fan_mode(static_cast(msg.fan_mode)); if (msg.has_custom_fan_mode) - call.set_fan_mode(msg.custom_fan_mode); + call.set_fan_mode(msg.custom_fan_mode.c_str()); if (msg.has_preset) call.set_preset(static_cast(msg.preset)); if (msg.has_custom_preset) - call.set_preset(msg.custom_preset); + call.set_preset(msg.custom_preset.c_str()); if (msg.has_swing_mode) call.set_swing_mode(static_cast(msg.swing_mode)); call.perform(); diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index dc83189692..ff97265d9e 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -232,13 +232,12 @@ ClimateCall &ClimateCall::set_preset(const char *custom_preset) { } } // Find the matching pointer from traits - const auto &supported = this->parent_->get_traits().get_supported_custom_presets(); - for (const char *preset : supported) { - if (strcmp(preset, custom_preset) == 0) { - this->custom_preset_ = preset; - this->preset_.reset(); - return *this; - } + auto traits = this->parent_->get_traits(); + const char *preset_ptr = traits.find_custom_preset(custom_preset); + if (preset_ptr != nullptr) { + this->custom_preset_ = preset_ptr; + this->preset_.reset(); + return *this; } ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), custom_preset); return *this; From 39beaae20f411f03a6fa006c00c05cbdf5de35f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 18:56:42 -0500 Subject: [PATCH 297/394] simplify --- esphome/components/climate/climate.cpp | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index ff97265d9e..20df78ea1f 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -635,13 +635,33 @@ bool Climate::set_fan_mode_(ClimateFanMode mode) { } bool Climate::set_custom_fan_mode_(const std::string &mode) { - return set_alternative(this->custom_fan_mode, this->fan_mode, mode.c_str()); + auto traits = this->get_traits(); + const char *mode_ptr = traits.find_custom_fan_mode(mode.c_str()); + if (mode_ptr != nullptr) { + return set_alternative(this->custom_fan_mode, this->fan_mode, mode_ptr); + } + // Mode not found in supported custom modes, clear it + if (this->custom_fan_mode != nullptr) { + this->custom_fan_mode = nullptr; + return true; + } + return false; } bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); } bool Climate::set_custom_preset_(const std::string &preset) { - return set_alternative(this->custom_preset, this->preset, preset.c_str()); + auto traits = this->get_traits(); + const char *preset_ptr = traits.find_custom_preset(preset.c_str()); + if (preset_ptr != nullptr) { + return set_alternative(this->custom_preset, this->preset, preset_ptr); + } + // Preset not found in supported custom presets, clear it + if (this->custom_preset != nullptr) { + this->custom_preset = nullptr; + return true; + } + return false; } void Climate::dump_traits_(const char *tag) { From b9d0e4061b0e2ce405a739a17805c213e0f9ac1a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 18:58:52 -0500 Subject: [PATCH 298/394] simplify --- esphome/components/climate/climate.h | 8 ++++---- esphome/components/climate/climate_traits.h | 21 +++++++++++---------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 49ea2a47a8..0600cf234c 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -110,12 +110,12 @@ class ClimateCall { const optional &get_fan_mode() const; const optional &get_swing_mode() const; const optional &get_preset() const; + /// @deprecated Use get_custom_fan_mode() (returns const char*) instead (since 2025.11.0) + optional get_custom_fan_mode() const; + /// @deprecated Use get_custom_preset() (returns const char*) instead (since 2025.11.0) + optional get_custom_preset() const; const char *get_custom_fan_mode() const; const char *get_custom_preset() const; - /// @deprecated Use get_custom_fan_mode() (returns const char*) instead (since 2025.11.0) - optional get_custom_fan_mode_optional() const; - /// @deprecated Use get_custom_preset() (returns const char*) instead (since 2025.11.0) - optional get_custom_preset_optional() const; protected: void validate_(); diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index e5171867d5..1fba56888f 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -19,6 +19,15 @@ inline bool vector_contains(const std::vector &vec, const char *va return false; } +// Find and return matching pointer from vector, or nullptr if not found +inline const char *vector_find(const std::vector &vec, const char *value) { + for (const char *item : vec) { + if (strcmp(item, value) == 0) + return item; + } + return nullptr; +} + // Type aliases for climate enum bitmasks // These replace std::set to eliminate red-black tree overhead // For contiguous enums starting at 0, DefaultBitPolicy provides 1:1 mapping (enum value = bit position) @@ -153,11 +162,7 @@ class ClimateTraits { } /// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found const char *find_custom_fan_mode(const char *custom_fan_mode) const { - for (const char *mode : this->supported_custom_fan_modes_) { - if (strcmp(mode, custom_fan_mode) == 0) - return mode; - } - return nullptr; + return vector_find(this->supported_custom_fan_modes_, custom_fan_mode); } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } @@ -184,11 +189,7 @@ class ClimateTraits { } /// Find and return the matching custom preset pointer from supported presets, or nullptr if not found const char *find_custom_preset(const char *custom_preset) const { - for (const char *preset : this->supported_custom_presets_) { - if (strcmp(preset, custom_preset) == 0) - return preset; - } - return nullptr; + return vector_find(this->supported_custom_presets_, custom_preset); } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } From f66f9c4eafe4360e17bc8d847d56b03446bd7b99 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:00:02 -0500 Subject: [PATCH 299/394] simplify --- esphome/components/climate/climate.cpp | 4 ++-- esphome/components/climate/climate.h | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 20df78ea1f..a9d42523d8 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -300,14 +300,14 @@ const optional &ClimateCall::get_preset() const { return this->pr const char *ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } const char *ClimateCall::get_custom_preset() const { return this->custom_preset_; } -optional ClimateCall::get_custom_fan_mode_optional() const { +optional ClimateCall::get_custom_fan_mode() const { if (this->custom_fan_mode_ != nullptr) { return std::string(this->custom_fan_mode_); } return {}; } -optional ClimateCall::get_custom_preset_optional() const { +optional ClimateCall::get_custom_preset() const { if (this->custom_preset_ != nullptr) { return std::string(this->custom_preset_); } diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 0600cf234c..49ea2a47a8 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -110,12 +110,12 @@ class ClimateCall { const optional &get_fan_mode() const; const optional &get_swing_mode() const; const optional &get_preset() const; - /// @deprecated Use get_custom_fan_mode() (returns const char*) instead (since 2025.11.0) - optional get_custom_fan_mode() const; - /// @deprecated Use get_custom_preset() (returns const char*) instead (since 2025.11.0) - optional get_custom_preset() const; const char *get_custom_fan_mode() const; const char *get_custom_preset() const; + /// @deprecated Use get_custom_fan_mode() (returns const char*) instead (since 2025.11.0) + optional get_custom_fan_mode_optional() const; + /// @deprecated Use get_custom_preset() (returns const char*) instead (since 2025.11.0) + optional get_custom_preset_optional() const; protected: void validate_(); From 952f6f5029a79780b7679e75d94c8f560e9ac496 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:01:48 -0500 Subject: [PATCH 300/394] simplify --- esphome/components/climate/climate.cpp | 2 -- esphome/components/climate/climate.h | 8 ++------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index a9d42523d8..dc5b411eaf 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -297,8 +297,6 @@ const optional &ClimateCall::get_mode() const { return this->mode_; const optional &ClimateCall::get_fan_mode() const { return this->fan_mode_; } const optional &ClimateCall::get_swing_mode() const { return this->swing_mode_; } const optional &ClimateCall::get_preset() const { return this->preset_; } -const char *ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } -const char *ClimateCall::get_custom_preset() const { return this->custom_preset_; } optional ClimateCall::get_custom_fan_mode() const { if (this->custom_fan_mode_ != nullptr) { diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 49ea2a47a8..7f1ac0a4aa 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -110,12 +110,8 @@ class ClimateCall { const optional &get_fan_mode() const; const optional &get_swing_mode() const; const optional &get_preset() const; - const char *get_custom_fan_mode() const; - const char *get_custom_preset() const; - /// @deprecated Use get_custom_fan_mode() (returns const char*) instead (since 2025.11.0) - optional get_custom_fan_mode_optional() const; - /// @deprecated Use get_custom_preset() (returns const char*) instead (since 2025.11.0) - optional get_custom_preset_optional() const; + optional get_custom_fan_mode() const; + optional get_custom_preset() const; protected: void validate_(); From 41bd8951dc8c05d3f2e9e3bc3f8ac009ed5bdd08 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:02:45 -0500 Subject: [PATCH 301/394] simplify --- esphome/components/api/api_connection.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 8730828994..5a33a82842 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -637,14 +637,14 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection } if (traits.get_supports_fan_modes() && climate->fan_mode.has_value()) resp.fan_mode = static_cast(climate->fan_mode.value()); - if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) { - resp.set_custom_fan_mode(StringRef(climate->custom_fan_mode.value())); + if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode != nullptr) { + resp.set_custom_fan_mode(StringRef(climate->custom_fan_mode)); } if (traits.get_supports_presets() && climate->preset.has_value()) { resp.preset = static_cast(climate->preset.value()); } - if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) { - resp.set_custom_preset(StringRef(climate->custom_preset.value())); + if (!traits.get_supported_custom_presets().empty() && climate->custom_preset != nullptr) { + resp.set_custom_preset(StringRef(climate->custom_preset)); } if (traits.get_supports_swing_modes()) resp.swing_mode = static_cast(climate->swing_mode); From 4565dcc4d9bc962c71c900aff3949059c0872422 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:03:01 -0500 Subject: [PATCH 302/394] simplify --- esphome/components/climate/climate.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 7f1ac0a4aa..5928df822e 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -74,11 +74,9 @@ class ClimateCall { /// Set the fan mode of the climate device. ClimateCall &set_fan_mode(optional fan_mode); /// Set the fan mode of the climate device based on a string. - __attribute__((deprecated("Use set_fan_mode(const char*) instead"))) ClimateCall &set_fan_mode( - const std::string &fan_mode); + ClimateCall &set_fan_mode(const std::string &fan_mode); /// Set the fan mode of the climate device based on a string. - __attribute__((deprecated("Use set_fan_mode(const char*) instead"))) ClimateCall &set_fan_mode( - optional fan_mode); + ClimateCall &set_fan_mode(optional fan_mode); /// Set the custom fan mode of the climate device. ClimateCall &set_fan_mode(const char *custom_fan_mode); /// Set the swing mode of the climate device. From 46e4fe28969eb674c60b853cbd27dd637c6b749b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:03:12 -0500 Subject: [PATCH 303/394] simplify --- esphome/components/climate/climate.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 5928df822e..e5d098291c 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -90,10 +90,9 @@ class ClimateCall { /// Set the preset of the climate device. ClimateCall &set_preset(optional preset); /// Set the preset of the climate device based on a string. - __attribute__((deprecated("Use set_preset(const char*) instead"))) ClimateCall &set_preset(const std::string &preset); + ClimateCall &set_preset(const std::string &preset); /// Set the preset of the climate device based on a string. - __attribute__((deprecated("Use set_preset(const char*) instead"))) ClimateCall &set_preset( - optional preset); + ClimateCall &set_preset(optional preset); /// Set the custom preset of the climate device. ClimateCall &set_preset(const char *custom_preset); From 8c90ea860cd0316cf51f69b6c50a7810fc8143b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:04:52 -0500 Subject: [PATCH 304/394] simplify --- esphome/components/climate/climate.cpp | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index dc5b411eaf..80027ee377 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -299,17 +299,11 @@ const optional &ClimateCall::get_swing_mode() const { return t const optional &ClimateCall::get_preset() const { return this->preset_; } optional ClimateCall::get_custom_fan_mode() const { - if (this->custom_fan_mode_ != nullptr) { - return std::string(this->custom_fan_mode_); - } - return {}; + return this->custom_fan_mode_ != nullptr ? std::string(this->custom_fan_mode_) : optional{}; } optional ClimateCall::get_custom_preset() const { - if (this->custom_preset_ != nullptr) { - return std::string(this->custom_preset_); - } - return {}; + return this->custom_preset_ != nullptr ? std::string(this->custom_preset_) : optional{}; } ClimateCall &ClimateCall::set_target_temperature_high(optional target_temperature_high) { From 1864cf6ad830d55377c3cbf652a0b1cb579b6319 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:08:40 -0500 Subject: [PATCH 305/394] simplify --- esphome/components/climate/climate.cpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 80027ee377..196269a736 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -192,14 +192,12 @@ ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode) { // Check if it's a standard enum mode first for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) { if (str_equals_case_insensitive(custom_fan_mode, mode_entry.str)) { - this->set_fan_mode(static_cast(mode_entry.value)); - return *this; + return this->set_fan_mode(static_cast(mode_entry.value)); } } // Find the matching pointer from traits auto traits = this->parent_->get_traits(); - const char *mode_ptr = traits.find_custom_fan_mode(custom_fan_mode); - if (mode_ptr != nullptr) { + if (const char *mode_ptr = traits.find_custom_fan_mode(custom_fan_mode)) { this->custom_fan_mode_ = mode_ptr; this->fan_mode_.reset(); return *this; @@ -227,14 +225,12 @@ ClimateCall &ClimateCall::set_preset(const char *custom_preset) { // Check if it's a standard enum preset first for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) { if (str_equals_case_insensitive(custom_preset, preset_entry.str)) { - this->set_preset(static_cast(preset_entry.value)); - return *this; + return this->set_preset(static_cast(preset_entry.value)); } } // Find the matching pointer from traits auto traits = this->parent_->get_traits(); - const char *preset_ptr = traits.find_custom_preset(custom_preset); - if (preset_ptr != nullptr) { + if (const char *preset_ptr = traits.find_custom_preset(custom_preset)) { this->custom_preset_ = preset_ptr; this->preset_.reset(); return *this; From af165539e667af0b5b0d988f5b2097bec848895f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:37:02 -0500 Subject: [PATCH 306/394] simplify --- esphome/components/climate/climate_traits.h | 26 +++++++++++++------ .../thermostat/thermostat_climate.cpp | 16 +++++++----- esphome/components/web_server/web_server.cpp | 10 +++---- 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 1fba56888f..869224b117 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -65,7 +65,13 @@ using ClimatePresetMask = FiniteSetMaskfeature_flags_; } @@ -160,10 +166,6 @@ class ClimateTraits { bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { return this->supports_custom_fan_mode(custom_fan_mode.c_str()); } - /// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found - const char *find_custom_fan_mode(const char *custom_fan_mode) const { - return vector_find(this->supported_custom_fan_modes_, custom_fan_mode); - } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); } @@ -187,10 +189,6 @@ class ClimateTraits { bool supports_custom_preset(const std::string &custom_preset) const { return this->supports_custom_preset(custom_preset.c_str()); } - /// Find and return the matching custom preset pointer from supported presets, or nullptr if not found - const char *find_custom_preset(const char *custom_preset) const { - return vector_find(this->supported_custom_presets_, custom_preset); - } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); } @@ -249,6 +247,18 @@ class ClimateTraits { } } + /// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found + /// This is protected as it's an implementation detail - use Climate::set_custom_fan_mode_() instead + const char *find_custom_fan_mode(const char *custom_fan_mode) const { + return vector_find(this->supported_custom_fan_modes_, custom_fan_mode); + } + + /// Find and return the matching custom preset pointer from supported presets, or nullptr if not found + /// This is protected as it's an implementation detail - use Climate::set_custom_preset_() instead + const char *find_custom_preset(const char *custom_preset) const { + return vector_find(this->supported_custom_presets_, custom_preset); + } + uint32_t feature_flags_{0}; float visual_min_temperature_{10}; float visual_max_temperature_{30}; diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 6842bd4be8..b5fce2f6fd 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -223,7 +223,8 @@ void ThermostatClimate::control(const climate::ClimateCall &call) { if (this->setup_complete_) { this->change_custom_preset_(call.get_custom_preset().value()); } else { - this->custom_preset = call.get_custom_preset().value(); + // Use the base class method which handles pointer lookup internally + this->set_custom_preset_(call.get_custom_preset().value()); } } @@ -1171,7 +1172,7 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { } else { ESP_LOGI(TAG, "No changes required to apply preset %s", LOG_STR_ARG(climate::climate_preset_to_string(preset))); } - this->custom_preset.reset(); + this->custom_preset = nullptr; this->preset = preset; } else { ESP_LOGW(TAG, "Preset %s not configured; ignoring", LOG_STR_ARG(climate::climate_preset_to_string(preset))); @@ -1183,11 +1184,12 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) if (config != this->custom_preset_config_.end()) { ESP_LOGV(TAG, "Custom preset %s requested", custom_preset.c_str()); - if (this->change_preset_internal_(config->second) || (!this->custom_preset.has_value()) || - this->custom_preset.value() != custom_preset) { + if (this->change_preset_internal_(config->second) || (this->custom_preset == nullptr) || + strcmp(this->custom_preset, custom_preset.c_str()) != 0) { // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; - this->custom_preset = custom_preset; + // Use the base class method which handles pointer lookup and preset reset internally + this->set_custom_preset_(custom_preset); if (trig != nullptr) { trig->trigger(); } @@ -1196,9 +1198,9 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) ESP_LOGI(TAG, "Custom preset %s applied", custom_preset.c_str()); } else { ESP_LOGI(TAG, "No changes required to apply custom preset %s", custom_preset.c_str()); + // Still need to ensure preset is reset and custom_preset is set + this->set_custom_preset_(custom_preset); } - this->preset.reset(); - this->custom_preset = custom_preset; } else { ESP_LOGW(TAG, "Custom preset %s not configured; ignoring", custom_preset.c_str()); } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 1d08ef5a35..ee626b8b9b 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1312,7 +1312,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf for (climate::ClimatePreset m : traits.get_supported_presets()) opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m))); } - if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) { + if (!traits.get_supported_custom_presets().empty() && obj->custom_preset != nullptr) { JsonArray opt = root["custom_presets"].to(); for (auto const &custom_preset : traits.get_supported_custom_presets()) opt.add(custom_preset); @@ -1333,14 +1333,14 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf if (traits.get_supports_fan_modes() && obj->fan_mode.has_value()) { root["fan_mode"] = PSTR_LOCAL(climate_fan_mode_to_string(obj->fan_mode.value())); } - if (!traits.get_supported_custom_fan_modes().empty() && obj->custom_fan_mode.has_value()) { - root["custom_fan_mode"] = obj->custom_fan_mode.value().c_str(); + if (!traits.get_supported_custom_fan_modes().empty() && obj->custom_fan_mode != nullptr) { + root["custom_fan_mode"] = obj->custom_fan_mode; } if (traits.get_supports_presets() && obj->preset.has_value()) { root["preset"] = PSTR_LOCAL(climate_preset_to_string(obj->preset.value())); } - if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) { - root["custom_preset"] = obj->custom_preset.value().c_str(); + if (!traits.get_supported_custom_presets().empty() && obj->custom_preset != nullptr) { + root["custom_preset"] = obj->custom_preset; } if (traits.get_supports_swing_modes()) { root["swing_mode"] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode)); From 56c6cc8c9f8cc7b4631764b4b8c60d1ca96b1c4e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:43:07 -0500 Subject: [PATCH 307/394] simplify --- esphome/components/climate/climate.cpp | 54 ++++++++++----------- esphome/components/climate/climate.h | 10 ++++ esphome/components/climate/climate_traits.h | 14 +++--- 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 196269a736..9b896a3a4b 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -195,9 +195,8 @@ ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode) { return this->set_fan_mode(static_cast(mode_entry.value)); } } - // Find the matching pointer from traits - auto traits = this->parent_->get_traits(); - if (const char *mode_ptr = traits.find_custom_fan_mode(custom_fan_mode)) { + // Find the matching pointer from parent climate device + if (const char *mode_ptr = this->parent_->find_custom_fan_mode_(custom_fan_mode)) { this->custom_fan_mode_ = mode_ptr; this->fan_mode_.reset(); return *this; @@ -228,9 +227,8 @@ ClimateCall &ClimateCall::set_preset(const char *custom_preset) { return this->set_preset(static_cast(preset_entry.value)); } } - // Find the matching pointer from traits - auto traits = this->parent_->get_traits(); - if (const char *preset_ptr = traits.find_custom_preset(custom_preset)) { + // Find the matching pointer from parent climate device + if (const char *preset_ptr = this->parent_->find_custom_preset_(custom_preset)) { this->custom_preset_ = preset_ptr; this->preset_.reset(); return *this; @@ -622,34 +620,34 @@ bool Climate::set_fan_mode_(ClimateFanMode mode) { return set_alternative(this->fan_mode, this->custom_fan_mode, mode); } -bool Climate::set_custom_fan_mode_(const std::string &mode) { +bool Climate::set_custom_fan_mode_(const char *mode) { auto traits = this->get_traits(); - const char *mode_ptr = traits.find_custom_fan_mode(mode.c_str()); - if (mode_ptr != nullptr) { - return set_alternative(this->custom_fan_mode, this->fan_mode, mode_ptr); - } - // Mode not found in supported custom modes, clear it - if (this->custom_fan_mode != nullptr) { - this->custom_fan_mode = nullptr; - return true; - } - return false; + const char *mode_ptr = traits.find_custom_fan_mode_(mode); + return mode_ptr != nullptr ? set_alternative(this->custom_fan_mode, this->fan_mode, mode_ptr) + : (this->custom_fan_mode != nullptr ? (this->custom_fan_mode = nullptr, true) : false); } +bool Climate::set_custom_fan_mode_(const std::string &mode) { return this->set_custom_fan_mode_(mode.c_str()); } + bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); } -bool Climate::set_custom_preset_(const std::string &preset) { +bool Climate::set_custom_preset_(const char *preset) { auto traits = this->get_traits(); - const char *preset_ptr = traits.find_custom_preset(preset.c_str()); - if (preset_ptr != nullptr) { - return set_alternative(this->custom_preset, this->preset, preset_ptr); - } - // Preset not found in supported custom presets, clear it - if (this->custom_preset != nullptr) { - this->custom_preset = nullptr; - return true; - } - return false; + const char *preset_ptr = traits.find_custom_preset_(preset); + return preset_ptr != nullptr ? set_alternative(this->custom_preset, this->preset, preset_ptr) + : (this->custom_preset != nullptr ? (this->custom_preset = nullptr, true) : false); +} + +bool Climate::set_custom_preset_(const std::string &preset) { return this->set_custom_preset_(preset.c_str()); } + +const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) { + auto traits = this->get_traits(); + return traits.find_custom_fan_mode_(custom_fan_mode); +} + +const char *Climate::find_custom_preset_(const char *custom_preset) { + auto traits = this->get_traits(); + return traits.find_custom_preset_(custom_preset); } void Climate::dump_traits_(const char *tag) { diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index e5d098291c..ea94128f5c 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -263,15 +263,25 @@ class Climate : public EntityBase { /// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed. bool set_fan_mode_(ClimateFanMode mode); + /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed. + bool set_custom_fan_mode_(const char *mode); /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed. bool set_custom_fan_mode_(const std::string &mode); /// Set preset. Reset custom preset. Return true if preset has been changed. bool set_preset_(ClimatePreset preset); + /// Set custom preset. Reset primary preset. Return true if preset has been changed. + bool set_custom_preset_(const char *preset); /// Set custom preset. Reset primary preset. Return true if preset has been changed. bool set_custom_preset_(const std::string &preset); + /// Find and return the matching custom fan mode pointer from traits, or nullptr if not found. + const char *find_custom_fan_mode_(const char *custom_fan_mode); + + /// Find and return the matching custom preset pointer from traits, or nullptr if not found. + const char *find_custom_preset_(const char *custom_preset); + /** Get the default traits of this climate device. * * Traits are static data that encode the capabilities and static data for a climate device such as supported diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 869224b117..65103cdaad 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -65,12 +65,10 @@ using ClimatePresetMask = FiniteSetMasksupported_custom_fan_modes_, custom_fan_mode); } /// Find and return the matching custom preset pointer from supported presets, or nullptr if not found - /// This is protected as it's an implementation detail - use Climate::set_custom_preset_() instead - const char *find_custom_preset(const char *custom_preset) const { + /// This is protected as it's an implementation detail - use Climate::find_custom_preset_() instead + const char *find_custom_preset_(const char *custom_preset) const { return vector_find(this->supported_custom_presets_, custom_preset); } From dda7b52f944a9cbcca865715fbe300dbdca8f37d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:44:30 -0500 Subject: [PATCH 308/394] simplify --- esphome/components/climate/climate_traits.h | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 65103cdaad..cbd9d1dbf4 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -9,6 +9,16 @@ namespace esphome { namespace climate { +// Type aliases for climate enum bitmasks +// These replace std::set to eliminate red-black tree overhead +// For contiguous enums starting at 0, DefaultBitPolicy provides 1:1 mapping (enum value = bit position) +// Bitmask size is automatically calculated from the last enum value +using ClimateModeMask = FiniteSetMask>; +using ClimateFanModeMask = FiniteSetMask>; +using ClimateSwingModeMask = + FiniteSetMask>; +using ClimatePresetMask = FiniteSetMask>; + // Lightweight linear search for small vectors (1-20 items) of const char* pointers // Avoids std::find template overhead inline bool vector_contains(const std::vector &vec, const char *value) { @@ -28,16 +38,6 @@ inline const char *vector_find(const std::vector &vec, const char return nullptr; } -// Type aliases for climate enum bitmasks -// These replace std::set to eliminate red-black tree overhead -// For contiguous enums starting at 0, DefaultBitPolicy provides 1:1 mapping (enum value = bit position) -// Bitmask size is automatically calculated from the last enum value -using ClimateModeMask = FiniteSetMask>; -using ClimateFanModeMask = FiniteSetMask>; -using ClimateSwingModeMask = - FiniteSetMask>; -using ClimatePresetMask = FiniteSetMask>; - /** This class contains all static data for climate devices. * * All climate devices must support these features: From 13148f2c893acf2dca62d2e5bd35b4d93e9fd064 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:47:45 -0500 Subject: [PATCH 309/394] simplify --- esphome/components/api/api_connection.cpp | 4 ++-- esphome/components/climate/climate.h | 6 ++++++ esphome/components/web_server/web_server.cpp | 6 +++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 5a33a82842..2914f15b4d 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -637,13 +637,13 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection } if (traits.get_supports_fan_modes() && climate->fan_mode.has_value()) resp.fan_mode = static_cast(climate->fan_mode.value()); - if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode != nullptr) { + if (!traits.get_supported_custom_fan_modes().empty() && climate->has_custom_fan_mode()) { resp.set_custom_fan_mode(StringRef(climate->custom_fan_mode)); } if (traits.get_supports_presets() && climate->preset.has_value()) { resp.preset = static_cast(climate->preset.value()); } - if (!traits.get_supported_custom_presets().empty() && climate->custom_preset != nullptr) { + if (!traits.get_supported_custom_presets().empty() && climate->has_custom_preset()) { resp.set_custom_preset(StringRef(climate->custom_preset)); } if (traits.get_supports_swing_modes()) diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index ea94128f5c..5166e19319 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -216,6 +216,12 @@ class Climate : public EntityBase { void set_visual_min_humidity_override(float visual_min_humidity_override); void set_visual_max_humidity_override(float visual_max_humidity_override); + /// Check if a custom fan mode is currently active. + bool has_custom_fan_mode() const { return this->custom_fan_mode != nullptr; } + + /// Check if a custom preset is currently active. + bool has_custom_preset() const { return this->custom_preset != nullptr; } + /// The current temperature of the climate device, as reported from the integration. float current_temperature{NAN}; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index ee626b8b9b..7901869b2f 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1312,7 +1312,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf for (climate::ClimatePreset m : traits.get_supported_presets()) opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m))); } - if (!traits.get_supported_custom_presets().empty() && obj->custom_preset != nullptr) { + if (!traits.get_supported_custom_presets().empty() && obj->has_custom_preset()) { JsonArray opt = root["custom_presets"].to(); for (auto const &custom_preset : traits.get_supported_custom_presets()) opt.add(custom_preset); @@ -1333,13 +1333,13 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf if (traits.get_supports_fan_modes() && obj->fan_mode.has_value()) { root["fan_mode"] = PSTR_LOCAL(climate_fan_mode_to_string(obj->fan_mode.value())); } - if (!traits.get_supported_custom_fan_modes().empty() && obj->custom_fan_mode != nullptr) { + if (!traits.get_supported_custom_fan_modes().empty() && obj->has_custom_fan_mode()) { root["custom_fan_mode"] = obj->custom_fan_mode; } if (traits.get_supports_presets() && obj->preset.has_value()) { root["preset"] = PSTR_LOCAL(climate_preset_to_string(obj->preset.value())); } - if (!traits.get_supported_custom_presets().empty() && obj->custom_preset != nullptr) { + if (!traits.get_supported_custom_presets().empty() && obj->has_custom_preset()) { root["custom_preset"] = obj->custom_preset; } if (traits.get_supports_swing_modes()) { From 219a318ee35773e81233396b05148ec9d67a37d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:50:11 -0500 Subject: [PATCH 310/394] simplify --- esphome/components/climate/climate.cpp | 8 ++++---- esphome/components/thermostat/thermostat_climate.cpp | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 9b896a3a4b..c48a94bb73 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -392,7 +392,7 @@ void Climate::save_state_() { state.uses_custom_fan_mode = false; state.fan_mode = this->fan_mode.value(); } - if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode != nullptr) { + if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) { state.uses_custom_fan_mode = true; const auto &supported = traits.get_supported_custom_fan_modes(); // std::vector maintains insertion order @@ -409,7 +409,7 @@ void Climate::save_state_() { state.uses_custom_preset = false; state.preset = this->preset.value(); } - if (!traits.get_supported_custom_presets().empty() && custom_preset != nullptr) { + if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) { state.uses_custom_preset = true; const auto &supported = traits.get_supported_custom_presets(); // std::vector maintains insertion order @@ -440,13 +440,13 @@ void Climate::publish_state() { if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) { ESP_LOGD(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value()))); } - if (!traits.get_supported_custom_fan_modes().empty() && this->custom_fan_mode != nullptr) { + if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) { ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode); } if (traits.get_supports_presets() && this->preset.has_value()) { ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value()))); } - if (!traits.get_supported_custom_presets().empty() && this->custom_preset != nullptr) { + if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) { ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset); } if (traits.get_supports_swing_modes()) { diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index b5fce2f6fd..1a53a66f77 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -1184,7 +1184,7 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) if (config != this->custom_preset_config_.end()) { ESP_LOGV(TAG, "Custom preset %s requested", custom_preset.c_str()); - if (this->change_preset_internal_(config->second) || (this->custom_preset == nullptr) || + if (this->change_preset_internal_(config->second) || !this->has_custom_preset() || strcmp(this->custom_preset, custom_preset.c_str()) != 0) { // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; From 34d2056413a2aab5edefa8f58d255e4989d89f99 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:51:54 -0500 Subject: [PATCH 311/394] simplify --- esphome/components/climate/climate.cpp | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index c48a94bb73..9f9a0ca5d6 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -623,8 +623,15 @@ bool Climate::set_fan_mode_(ClimateFanMode mode) { bool Climate::set_custom_fan_mode_(const char *mode) { auto traits = this->get_traits(); const char *mode_ptr = traits.find_custom_fan_mode_(mode); - return mode_ptr != nullptr ? set_alternative(this->custom_fan_mode, this->fan_mode, mode_ptr) - : (this->custom_fan_mode != nullptr ? (this->custom_fan_mode = nullptr, true) : false); + if (mode_ptr != nullptr) { + return set_alternative(this->custom_fan_mode, this->fan_mode, mode_ptr); + } + // Mode not found in supported custom modes, clear it if currently set + if (this->has_custom_fan_mode()) { + this->custom_fan_mode = nullptr; + return true; + } + return false; } bool Climate::set_custom_fan_mode_(const std::string &mode) { return this->set_custom_fan_mode_(mode.c_str()); } @@ -634,8 +641,15 @@ bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->p bool Climate::set_custom_preset_(const char *preset) { auto traits = this->get_traits(); const char *preset_ptr = traits.find_custom_preset_(preset); - return preset_ptr != nullptr ? set_alternative(this->custom_preset, this->preset, preset_ptr) - : (this->custom_preset != nullptr ? (this->custom_preset = nullptr, true) : false); + if (preset_ptr != nullptr) { + return set_alternative(this->custom_preset, this->preset, preset_ptr); + } + // Preset not found in supported custom presets, clear it if currently set + if (this->has_custom_preset()) { + this->custom_preset = nullptr; + return true; + } + return false; } bool Climate::set_custom_preset_(const std::string &preset) { return this->set_custom_preset_(preset.c_str()); } From 5013b7be87d26b16acf72b6a8dedaf9190484fe8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 19:55:46 -0500 Subject: [PATCH 312/394] simplify --- esphome/components/climate/climate.h | 22 ++++++++++++------- .../thermostat/thermostat_climate.cpp | 2 -- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 5166e19319..495d9f700f 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -217,10 +217,10 @@ class Climate : public EntityBase { void set_visual_max_humidity_override(float visual_max_humidity_override); /// Check if a custom fan mode is currently active. - bool has_custom_fan_mode() const { return this->custom_fan_mode != nullptr; } + bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; } /// Check if a custom preset is currently active. - bool has_custom_preset() const { return this->custom_preset != nullptr; } + bool has_custom_preset() const { return this->custom_preset_ != nullptr; } /// The current temperature of the climate device, as reported from the integration. float current_temperature{NAN}; @@ -248,12 +248,6 @@ class Climate : public EntityBase { /// The active preset of the climate device. optional preset; - /// The active custom fan mode of the climate device. - const char *custom_fan_mode{nullptr}; - - /// The active custom preset mode of the climate device. - const char *custom_preset{nullptr}; - /// The active mode of the climate device. ClimateMode mode{CLIMATE_MODE_OFF}; @@ -263,6 +257,12 @@ class Climate : public EntityBase { /// The active swing mode of the climate device. ClimateSwingMode swing_mode{CLIMATE_SWING_OFF}; + /// Get the active custom fan mode (read-only access). + const char *get_custom_fan_mode() const { return this->custom_fan_mode_; } + + /// Get the active custom preset (read-only access). + const char *get_custom_preset() const { return this->custom_preset_; } + protected: friend ClimateCall; @@ -323,6 +323,12 @@ class Climate : public EntityBase { optional visual_current_temperature_step_override_{}; optional visual_min_humidity_override_{}; optional visual_max_humidity_override_{}; + + /// The active custom fan mode of the climate device (protected - use get_custom_fan_mode() or setters). + const char *custom_fan_mode_{nullptr}; + + /// The active custom preset mode of the climate device (protected - use get_custom_preset() or setters). + const char *custom_preset_{nullptr}; }; } // namespace climate diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 1a53a66f77..4e9c7e4d71 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -1198,8 +1198,6 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) ESP_LOGI(TAG, "Custom preset %s applied", custom_preset.c_str()); } else { ESP_LOGI(TAG, "No changes required to apply custom preset %s", custom_preset.c_str()); - // Still need to ensure preset is reset and custom_preset is set - this->set_custom_preset_(custom_preset); } } else { ESP_LOGW(TAG, "Custom preset %s not configured; ignoring", custom_preset.c_str()); From cd513b0672879b890b3790a82ee295393f7f128e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:02:28 -0500 Subject: [PATCH 313/394] simplify --- esphome/components/api/api_connection.cpp | 4 +-- .../bedjet/climate/bedjet_climate.cpp | 25 +++++++-------- esphome/components/climate/climate.cpp | 32 +++++++++++-------- esphome/components/climate/climate.h | 4 +++ .../thermostat/thermostat_climate.cpp | 4 +-- esphome/components/web_server/web_server.cpp | 4 +-- 6 files changed, 39 insertions(+), 34 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 2914f15b4d..7413b0c419 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -638,13 +638,13 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection if (traits.get_supports_fan_modes() && climate->fan_mode.has_value()) resp.fan_mode = static_cast(climate->fan_mode.value()); if (!traits.get_supported_custom_fan_modes().empty() && climate->has_custom_fan_mode()) { - resp.set_custom_fan_mode(StringRef(climate->custom_fan_mode)); + resp.set_custom_fan_mode(StringRef(climate->get_custom_fan_mode())); } if (traits.get_supports_presets() && climate->preset.has_value()) { resp.preset = static_cast(climate->preset.value()); } if (!traits.get_supported_custom_presets().empty() && climate->has_custom_preset()) { - resp.set_custom_preset(StringRef(climate->custom_preset)); + resp.set_custom_preset(StringRef(climate->get_custom_preset())); } if (traits.get_supports_swing_modes()) resp.swing_mode = static_cast(climate->swing_mode); diff --git a/esphome/components/bedjet/climate/bedjet_climate.cpp b/esphome/components/bedjet/climate/bedjet_climate.cpp index 65fa092e8e..302229f254 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.cpp +++ b/esphome/components/bedjet/climate/bedjet_climate.cpp @@ -79,7 +79,7 @@ void BedJetClimate::reset_state_() { this->target_temperature = NAN; this->current_temperature = NAN; this->preset.reset(); - this->custom_preset.reset(); + this->clear_custom_preset_(); this->publish_state(); } @@ -184,8 +184,7 @@ void BedJetClimate::control(const ClimateCall &call) { } if (result) { - this->custom_preset = preset; - this->preset.reset(); + this->set_custom_preset_(preset.c_str()); } } @@ -207,8 +206,7 @@ void BedJetClimate::control(const ClimateCall &call) { } if (result) { - this->fan_mode = fan_mode; - this->custom_fan_mode.reset(); + this->set_fan_mode_(fan_mode); } } else if (call.get_custom_fan_mode().has_value()) { auto fan_mode = *call.get_custom_fan_mode(); @@ -218,8 +216,7 @@ void BedJetClimate::control(const ClimateCall &call) { fan_index); bool result = this->parent_->set_fan_index(fan_index); if (result) { - this->custom_fan_mode = fan_mode; - this->fan_mode.reset(); + this->set_custom_fan_mode_(fan_mode.c_str()); } } } @@ -245,7 +242,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step); if (fan_mode_name != nullptr) { - this->custom_fan_mode = *fan_mode_name; + this->set_custom_fan_mode_(fan_mode_name); } // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any. @@ -255,7 +252,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { this->mode = CLIMATE_MODE_OFF; this->action = CLIMATE_ACTION_IDLE; this->fan_mode = CLIMATE_FAN_OFF; - this->custom_preset.reset(); + this->clear_custom_preset_(); this->preset.reset(); break; @@ -266,7 +263,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { if (this->heating_mode_ == HEAT_MODE_EXTENDED) { this->set_custom_preset_("LTD HT"); } else { - this->custom_preset.reset(); + this->clear_custom_preset_(); } break; @@ -275,7 +272,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { this->action = CLIMATE_ACTION_HEATING; this->preset.reset(); if (this->heating_mode_ == HEAT_MODE_EXTENDED) { - this->custom_preset.reset(); + this->clear_custom_preset_(); } else { this->set_custom_preset_("EXT HT"); } @@ -284,20 +281,20 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { case MODE_COOL: this->mode = CLIMATE_MODE_FAN_ONLY; this->action = CLIMATE_ACTION_COOLING; - this->custom_preset.reset(); + this->clear_custom_preset_(); this->preset.reset(); break; case MODE_DRY: this->mode = CLIMATE_MODE_DRY; this->action = CLIMATE_ACTION_DRYING; - this->custom_preset.reset(); + this->clear_custom_preset_(); this->preset.reset(); break; case MODE_TURBO: this->preset = CLIMATE_PRESET_BOOST; - this->custom_preset.reset(); + this->clear_custom_preset_(); this->mode = CLIMATE_MODE_HEAT; this->action = CLIMATE_ACTION_HEATING; break; diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 9f9a0ca5d6..5bf32e4c28 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -398,7 +398,7 @@ void Climate::save_state_() { // std::vector maintains insertion order size_t i = 0; for (const char *mode : supported) { - if (strcmp(mode, custom_fan_mode) == 0) { + if (strcmp(mode, this->custom_fan_mode_) == 0) { state.custom_fan_mode = i; break; } @@ -415,7 +415,7 @@ void Climate::save_state_() { // std::vector maintains insertion order size_t i = 0; for (const char *preset : supported) { - if (strcmp(preset, custom_preset) == 0) { + if (strcmp(preset, this->custom_preset_) == 0) { state.custom_preset = i; break; } @@ -441,13 +441,13 @@ void Climate::publish_state() { ESP_LOGD(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value()))); } if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) { - ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode); + ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode_); } if (traits.get_supports_presets() && this->preset.has_value()) { ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value()))); } if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) { - ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset); + ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_); } if (traits.get_supports_swing_modes()) { ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode))); @@ -572,20 +572,20 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { if (this->uses_custom_fan_mode) { if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { climate->fan_mode.reset(); - climate->custom_fan_mode = traits.get_supported_custom_fan_modes()[this->custom_fan_mode]; + climate->custom_fan_mode_ = traits.get_supported_custom_fan_modes()[this->custom_fan_mode]; } } else if (traits.supports_fan_mode(this->fan_mode)) { climate->fan_mode = this->fan_mode; - climate->custom_fan_mode = nullptr; + climate->clear_custom_fan_mode_(); } if (this->uses_custom_preset) { if (this->custom_preset < traits.get_supported_custom_presets().size()) { climate->preset.reset(); - climate->custom_preset = traits.get_supported_custom_presets()[this->custom_preset]; + climate->custom_preset_ = traits.get_supported_custom_presets()[this->custom_preset]; } } else if (traits.supports_preset(this->preset)) { climate->preset = this->preset; - climate->custom_preset = nullptr; + climate->clear_custom_preset_(); } if (traits.supports_swing_mode(this->swing_mode)) { climate->swing_mode = this->swing_mode; @@ -617,18 +617,18 @@ template bool set_alternative(T1 &dst, T2 } bool Climate::set_fan_mode_(ClimateFanMode mode) { - return set_alternative(this->fan_mode, this->custom_fan_mode, mode); + return set_alternative(this->fan_mode, this->custom_fan_mode_, mode); } bool Climate::set_custom_fan_mode_(const char *mode) { auto traits = this->get_traits(); const char *mode_ptr = traits.find_custom_fan_mode_(mode); if (mode_ptr != nullptr) { - return set_alternative(this->custom_fan_mode, this->fan_mode, mode_ptr); + return set_alternative(this->custom_fan_mode_, this->fan_mode, mode_ptr); } // Mode not found in supported custom modes, clear it if currently set if (this->has_custom_fan_mode()) { - this->custom_fan_mode = nullptr; + this->custom_fan_mode_ = nullptr; return true; } return false; @@ -636,17 +636,19 @@ bool Climate::set_custom_fan_mode_(const char *mode) { bool Climate::set_custom_fan_mode_(const std::string &mode) { return this->set_custom_fan_mode_(mode.c_str()); } -bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); } +void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; } + +bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset_, preset); } bool Climate::set_custom_preset_(const char *preset) { auto traits = this->get_traits(); const char *preset_ptr = traits.find_custom_preset_(preset); if (preset_ptr != nullptr) { - return set_alternative(this->custom_preset, this->preset, preset_ptr); + return set_alternative(this->custom_preset_, this->preset, preset_ptr); } // Preset not found in supported custom presets, clear it if currently set if (this->has_custom_preset()) { - this->custom_preset = nullptr; + this->custom_preset_ = nullptr; return true; } return false; @@ -654,6 +656,8 @@ bool Climate::set_custom_preset_(const char *preset) { bool Climate::set_custom_preset_(const std::string &preset) { return this->set_custom_preset_(preset.c_str()); } +void Climate::clear_custom_preset_() { this->custom_preset_ = nullptr; } + const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) { auto traits = this->get_traits(); return traits.find_custom_fan_mode_(custom_fan_mode); diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 495d9f700f..0c3393028a 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -273,6 +273,8 @@ class Climate : public EntityBase { bool set_custom_fan_mode_(const char *mode); /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed. bool set_custom_fan_mode_(const std::string &mode); + /// Clear custom fan mode. + void clear_custom_fan_mode_(); /// Set preset. Reset custom preset. Return true if preset has been changed. bool set_preset_(ClimatePreset preset); @@ -281,6 +283,8 @@ class Climate : public EntityBase { bool set_custom_preset_(const char *preset); /// Set custom preset. Reset primary preset. Return true if preset has been changed. bool set_custom_preset_(const std::string &preset); + /// Clear custom preset. + void clear_custom_preset_(); /// Find and return the matching custom fan mode pointer from traits, or nullptr if not found. const char *find_custom_fan_mode_(const char *custom_fan_mode); diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 4e9c7e4d71..2c8e3e4d9a 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -1172,7 +1172,7 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { } else { ESP_LOGI(TAG, "No changes required to apply preset %s", LOG_STR_ARG(climate::climate_preset_to_string(preset))); } - this->custom_preset = nullptr; + this->clear_custom_preset_(); this->preset = preset; } else { ESP_LOGW(TAG, "Preset %s not configured; ignoring", LOG_STR_ARG(climate::climate_preset_to_string(preset))); @@ -1185,7 +1185,7 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) if (config != this->custom_preset_config_.end()) { ESP_LOGV(TAG, "Custom preset %s requested", custom_preset.c_str()); if (this->change_preset_internal_(config->second) || !this->has_custom_preset() || - strcmp(this->custom_preset, custom_preset.c_str()) != 0) { + strcmp(this->get_custom_preset(), custom_preset.c_str()) != 0) { // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; // Use the base class method which handles pointer lookup and preset reset internally diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 7901869b2f..a1bba22cdb 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1334,13 +1334,13 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf root["fan_mode"] = PSTR_LOCAL(climate_fan_mode_to_string(obj->fan_mode.value())); } if (!traits.get_supported_custom_fan_modes().empty() && obj->has_custom_fan_mode()) { - root["custom_fan_mode"] = obj->custom_fan_mode; + root["custom_fan_mode"] = obj->get_custom_fan_mode(); } if (traits.get_supports_presets() && obj->preset.has_value()) { root["preset"] = PSTR_LOCAL(climate_preset_to_string(obj->preset.value())); } if (!traits.get_supported_custom_presets().empty() && obj->has_custom_preset()) { - root["custom_preset"] = obj->custom_preset; + root["custom_preset"] = obj->get_custom_preset(); } if (traits.get_supports_swing_modes()) { root["swing_mode"] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode)); From b4045b09632b4fe5f9d124f879597437d5f5f840 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:04:55 -0500 Subject: [PATCH 314/394] simplify --- esphome/components/climate/climate.cpp | 4 ++-- esphome/components/demo/demo_climate.h | 16 ++++++---------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 5bf32e4c28..c95fcd90b7 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -628,7 +628,7 @@ bool Climate::set_custom_fan_mode_(const char *mode) { } // Mode not found in supported custom modes, clear it if currently set if (this->has_custom_fan_mode()) { - this->custom_fan_mode_ = nullptr; + this->clear_custom_fan_mode_(); return true; } return false; @@ -648,7 +648,7 @@ bool Climate::set_custom_preset_(const char *preset) { } // Preset not found in supported custom presets, clear it if currently set if (this->has_custom_preset()) { - this->custom_preset_ = nullptr; + this->clear_custom_preset_(); return true; } return false; diff --git a/esphome/components/demo/demo_climate.h b/esphome/components/demo/demo_climate.h index 84b16e7ec5..0a71ec6dab 100644 --- a/esphome/components/demo/demo_climate.h +++ b/esphome/components/demo/demo_climate.h @@ -28,14 +28,14 @@ class DemoClimate : public climate::Climate, public Component { this->mode = climate::CLIMATE_MODE_AUTO; this->action = climate::CLIMATE_ACTION_COOLING; this->fan_mode = climate::CLIMATE_FAN_HIGH; - this->custom_preset = {"My Preset"}; + this->set_custom_preset_("My Preset"); break; case DemoClimateType::TYPE_3: this->current_temperature = 21.5; this->target_temperature_low = 21.0; this->target_temperature_high = 22.5; this->mode = climate::CLIMATE_MODE_HEAT_COOL; - this->custom_fan_mode = {"Auto Low"}; + this->set_custom_fan_mode_("Auto Low"); this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; this->preset = climate::CLIMATE_PRESET_AWAY; break; @@ -58,23 +58,19 @@ class DemoClimate : public climate::Climate, public Component { this->target_temperature_high = *call.get_target_temperature_high(); } if (call.get_fan_mode().has_value()) { - this->fan_mode = *call.get_fan_mode(); - this->custom_fan_mode.reset(); + this->set_fan_mode_(*call.get_fan_mode()); } if (call.get_swing_mode().has_value()) { this->swing_mode = *call.get_swing_mode(); } if (call.get_custom_fan_mode().has_value()) { - this->custom_fan_mode = *call.get_custom_fan_mode(); - this->fan_mode.reset(); + this->set_custom_fan_mode_(call.get_custom_fan_mode()->c_str()); } if (call.get_preset().has_value()) { - this->preset = *call.get_preset(); - this->custom_preset.reset(); + this->set_preset_(*call.get_preset()); } if (call.get_custom_preset().has_value()) { - this->custom_preset = *call.get_custom_preset(); - this->preset.reset(); + this->set_custom_preset_(call.get_custom_preset()->c_str()); } this->publish_state(); } From 70ec33f41840e2d84cf4ef4b35961acc9b8e55d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:07:33 -0500 Subject: [PATCH 315/394] simplify --- esphome/components/bedjet/climate/bedjet_climate.cpp | 8 ++++---- esphome/components/climate/climate.h | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/esphome/components/bedjet/climate/bedjet_climate.cpp b/esphome/components/bedjet/climate/bedjet_climate.cpp index 302229f254..737000f9ae 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.cpp +++ b/esphome/components/bedjet/climate/bedjet_climate.cpp @@ -120,7 +120,7 @@ void BedJetClimate::control(const ClimateCall &call) { if (button_result) { this->mode = mode; // We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those - this->custom_preset.reset(); + this->clear_custom_preset_(); this->preset.reset(); } } @@ -145,7 +145,7 @@ void BedJetClimate::control(const ClimateCall &call) { if (result) { this->mode = CLIMATE_MODE_HEAT; this->preset = CLIMATE_PRESET_BOOST; - this->custom_preset.reset(); + this->clear_custom_preset_(); } } else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) { if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) { @@ -153,7 +153,7 @@ void BedJetClimate::control(const ClimateCall &call) { result = this->parent_->send_button(heat_button(this->heating_mode_)); if (result) { this->preset.reset(); - this->custom_preset.reset(); + this->clear_custom_preset_(); } } else { ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'", @@ -242,7 +242,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step); if (fan_mode_name != nullptr) { - this->set_custom_fan_mode_(fan_mode_name); + this->set_custom_fan_mode_(fan_mode_name->c_str()); } // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any. diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 0c3393028a..8e2bd67995 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -265,6 +265,7 @@ class Climate : public EntityBase { protected: friend ClimateCall; + friend struct ClimateDeviceRestoreState; /// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed. bool set_fan_mode_(ClimateFanMode mode); From 03ec52752bc42e4acee70a3a0589ec60440e52b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:09:45 -0500 Subject: [PATCH 316/394] simplify --- esphome/components/climate/climate.cpp | 42 +++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index c95fcd90b7..f0f50973ab 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -617,14 +617,30 @@ template bool set_alternative(T1 &dst, T2 } bool Climate::set_fan_mode_(ClimateFanMode mode) { - return set_alternative(this->fan_mode, this->custom_fan_mode_, mode); + // Clear the custom fan mode (mutual exclusion) + bool changed = this->custom_fan_mode_ != nullptr; + this->custom_fan_mode_ = nullptr; + // Set the primary fan mode + if (changed || !this->fan_mode.has_value() || this->fan_mode.value() != mode) { + this->fan_mode = mode; + return true; + } + return false; } bool Climate::set_custom_fan_mode_(const char *mode) { auto traits = this->get_traits(); const char *mode_ptr = traits.find_custom_fan_mode_(mode); if (mode_ptr != nullptr) { - return set_alternative(this->custom_fan_mode_, this->fan_mode, mode_ptr); + // Clear the primary fan mode (mutual exclusion) + bool changed = this->fan_mode.has_value(); + this->fan_mode.reset(); + // Set the custom fan mode + if (changed || this->custom_fan_mode_ != mode_ptr) { + this->custom_fan_mode_ = mode_ptr; + return true; + } + return false; } // Mode not found in supported custom modes, clear it if currently set if (this->has_custom_fan_mode()) { @@ -638,13 +654,31 @@ bool Climate::set_custom_fan_mode_(const std::string &mode) { return this->set_c void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; } -bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset_, preset); } +bool Climate::set_preset_(ClimatePreset preset) { + // Clear the custom preset (mutual exclusion) + bool changed = this->custom_preset_ != nullptr; + this->custom_preset_ = nullptr; + // Set the primary preset + if (changed || !this->preset.has_value() || this->preset.value() != preset) { + this->preset = preset; + return true; + } + return false; +} bool Climate::set_custom_preset_(const char *preset) { auto traits = this->get_traits(); const char *preset_ptr = traits.find_custom_preset_(preset); if (preset_ptr != nullptr) { - return set_alternative(this->custom_preset_, this->preset, preset_ptr); + // Clear the primary preset (mutual exclusion) + bool changed = this->preset.has_value(); + this->preset.reset(); + // Set the custom preset + if (changed || this->custom_preset_ != preset_ptr) { + this->custom_preset_ = preset_ptr; + return true; + } + return false; } // Preset not found in supported custom presets, clear it if currently set if (this->has_custom_preset()) { From 60a303adb83eef202141a883ed5545fb7ddc88dd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:10:36 -0500 Subject: [PATCH 317/394] simplify --- esphome/components/climate/climate.cpp | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index f0f50973ab..36c407242a 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -593,29 +593,6 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { climate->publish_state(); } -// Generic template to set one value while clearing its alternative (mutual exclusion) -// Handles both optional and const char* types automatically using compile-time type detection -template bool set_alternative(T1 &dst, T2 &alt, T3 src) { - bool is_changed = false; - - // Clear the alternative based on its type (pointer or optional) - if constexpr (std::is_pointer_v>) { - is_changed = (alt != nullptr); - alt = nullptr; - } else { - is_changed = alt.has_value(); - alt.reset(); - } - - // Set the destination value - if (is_changed || dst != src) { - dst = src; - is_changed = true; - } - - return is_changed; -} - bool Climate::set_fan_mode_(ClimateFanMode mode) { // Clear the custom fan mode (mutual exclusion) bool changed = this->custom_fan_mode_ != nullptr; From d1bb5c4d790aeab887de7425f7b3c52968a94695 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:16:36 -0500 Subject: [PATCH 318/394] simplify --- esphome/components/climate/climate.cpp | 83 +++++++++++--------------- 1 file changed, 36 insertions(+), 47 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 36c407242a..fc26ff524a 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -593,76 +593,65 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { climate->publish_state(); } -bool Climate::set_fan_mode_(ClimateFanMode mode) { - // Clear the custom fan mode (mutual exclusion) - bool changed = this->custom_fan_mode_ != nullptr; - this->custom_fan_mode_ = nullptr; - // Set the primary fan mode - if (changed || !this->fan_mode.has_value() || this->fan_mode.value() != mode) { - this->fan_mode = mode; +// Template helper for setting primary modes with mutual exclusion +// Clears custom pointer and sets primary optional value +template bool set_primary_mode_(optional &primary, const char *&custom_ptr, T value) { + // Clear the custom mode (mutual exclusion) + bool changed = custom_ptr != nullptr; + custom_ptr = nullptr; + // Set the primary mode + if (changed || !primary.has_value() || primary.value() != value) { + primary = value; return true; } return false; } -bool Climate::set_custom_fan_mode_(const char *mode) { - auto traits = this->get_traits(); - const char *mode_ptr = traits.find_custom_fan_mode_(mode); - if (mode_ptr != nullptr) { - // Clear the primary fan mode (mutual exclusion) - bool changed = this->fan_mode.has_value(); - this->fan_mode.reset(); - // Set the custom fan mode - if (changed || this->custom_fan_mode_ != mode_ptr) { - this->custom_fan_mode_ = mode_ptr; +// Template helper for setting custom modes with mutual exclusion +// Takes pre-computed values: the found pointer from traits and whether custom mode is currently set +template +bool set_custom_mode_(const char *&custom_ptr, optional &primary, const char *found_ptr, bool has_custom) { + if (found_ptr != nullptr) { + // Clear the primary mode (mutual exclusion) + bool changed = primary.has_value(); + primary.reset(); + // Set the custom mode + if (changed || custom_ptr != found_ptr) { + custom_ptr = found_ptr; return true; } return false; } - // Mode not found in supported custom modes, clear it if currently set - if (this->has_custom_fan_mode()) { - this->clear_custom_fan_mode_(); + // Mode not found in supported modes, clear it if currently set + if (has_custom) { + custom_ptr = nullptr; return true; } return false; } +bool Climate::set_fan_mode_(ClimateFanMode mode) { + return set_primary_mode_(this->fan_mode, this->custom_fan_mode_, mode); +} + +bool Climate::set_custom_fan_mode_(const char *mode) { + auto traits = this->get_traits(); + return set_custom_mode_(this->custom_fan_mode_, this->fan_mode, traits.find_custom_fan_mode_(mode), + this->has_custom_fan_mode()); +} + bool Climate::set_custom_fan_mode_(const std::string &mode) { return this->set_custom_fan_mode_(mode.c_str()); } void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; } bool Climate::set_preset_(ClimatePreset preset) { - // Clear the custom preset (mutual exclusion) - bool changed = this->custom_preset_ != nullptr; - this->custom_preset_ = nullptr; - // Set the primary preset - if (changed || !this->preset.has_value() || this->preset.value() != preset) { - this->preset = preset; - return true; - } - return false; + return set_primary_mode_(this->preset, this->custom_preset_, preset); } bool Climate::set_custom_preset_(const char *preset) { auto traits = this->get_traits(); - const char *preset_ptr = traits.find_custom_preset_(preset); - if (preset_ptr != nullptr) { - // Clear the primary preset (mutual exclusion) - bool changed = this->preset.has_value(); - this->preset.reset(); - // Set the custom preset - if (changed || this->custom_preset_ != preset_ptr) { - this->custom_preset_ = preset_ptr; - return true; - } - return false; - } - // Preset not found in supported custom presets, clear it if currently set - if (this->has_custom_preset()) { - this->clear_custom_preset_(); - return true; - } - return false; + return set_custom_mode_(this->custom_preset_, this->preset, traits.find_custom_preset_(preset), + this->has_custom_preset()); } bool Climate::set_custom_preset_(const std::string &preset) { return this->set_custom_preset_(preset.c_str()); } From a073ec4e11c5db0d27b096a0addb68cc2cff92ea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:19:07 -0500 Subject: [PATCH 319/394] simplify --- esphome/components/climate/climate.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index fc26ff524a..3656f57cc2 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -659,13 +659,11 @@ bool Climate::set_custom_preset_(const std::string &preset) { return this->set_c void Climate::clear_custom_preset_() { this->custom_preset_ = nullptr; } const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) { - auto traits = this->get_traits(); - return traits.find_custom_fan_mode_(custom_fan_mode); + return this->get_traits().find_custom_fan_mode_(custom_fan_mode); } const char *Climate::find_custom_preset_(const char *custom_preset) { - auto traits = this->get_traits(); - return traits.find_custom_preset_(custom_preset); + return this->get_traits().find_custom_preset_(custom_preset); } void Climate::dump_traits_(const char *tag) { From 6dd29f1917847736cd4607653d3c558d56a9fe6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:25:26 -0500 Subject: [PATCH 320/394] simplify --- esphome/components/climate/climate.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 3656f57cc2..07c75fada7 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -595,7 +595,7 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { // Template helper for setting primary modes with mutual exclusion // Clears custom pointer and sets primary optional value -template bool set_primary_mode_(optional &primary, const char *&custom_ptr, T value) { +template bool set_primary_mode(optional &primary, const char *&custom_ptr, T value) { // Clear the custom mode (mutual exclusion) bool changed = custom_ptr != nullptr; custom_ptr = nullptr; @@ -610,7 +610,7 @@ template bool set_primary_mode_(optional &primary, const char *&c // Template helper for setting custom modes with mutual exclusion // Takes pre-computed values: the found pointer from traits and whether custom mode is currently set template -bool set_custom_mode_(const char *&custom_ptr, optional &primary, const char *found_ptr, bool has_custom) { +bool set_custom_mode(const char *&custom_ptr, optional &primary, const char *found_ptr, bool has_custom) { if (found_ptr != nullptr) { // Clear the primary mode (mutual exclusion) bool changed = primary.has_value(); @@ -631,27 +631,25 @@ bool set_custom_mode_(const char *&custom_ptr, optional &primary, const char } bool Climate::set_fan_mode_(ClimateFanMode mode) { - return set_primary_mode_(this->fan_mode, this->custom_fan_mode_, mode); + return set_primary_mode(this->fan_mode, this->custom_fan_mode_, mode); } bool Climate::set_custom_fan_mode_(const char *mode) { auto traits = this->get_traits(); - return set_custom_mode_(this->custom_fan_mode_, this->fan_mode, traits.find_custom_fan_mode_(mode), - this->has_custom_fan_mode()); + return set_custom_mode(this->custom_fan_mode_, this->fan_mode, traits.find_custom_fan_mode_(mode), + this->has_custom_fan_mode()); } bool Climate::set_custom_fan_mode_(const std::string &mode) { return this->set_custom_fan_mode_(mode.c_str()); } void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; } -bool Climate::set_preset_(ClimatePreset preset) { - return set_primary_mode_(this->preset, this->custom_preset_, preset); -} +bool Climate::set_preset_(ClimatePreset preset) { return set_primary_mode(this->preset, this->custom_preset_, preset); } bool Climate::set_custom_preset_(const char *preset) { auto traits = this->get_traits(); - return set_custom_mode_(this->custom_preset_, this->preset, traits.find_custom_preset_(preset), - this->has_custom_preset()); + return set_custom_mode(this->custom_preset_, this->preset, traits.find_custom_preset_(preset), + this->has_custom_preset()); } bool Climate::set_custom_preset_(const std::string &preset) { return this->set_custom_preset_(preset.c_str()); } From 0a86254b8444ccd4804d699476bfa610fda4b150 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:32:28 -0500 Subject: [PATCH 321/394] simplify --- esphome/components/climate/climate.cpp | 4 ---- esphome/components/climate/climate.h | 4 ---- esphome/components/thermostat/thermostat_climate.cpp | 4 ++-- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 07c75fada7..ebc9e466e0 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -640,8 +640,6 @@ bool Climate::set_custom_fan_mode_(const char *mode) { this->has_custom_fan_mode()); } -bool Climate::set_custom_fan_mode_(const std::string &mode) { return this->set_custom_fan_mode_(mode.c_str()); } - void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; } bool Climate::set_preset_(ClimatePreset preset) { return set_primary_mode(this->preset, this->custom_preset_, preset); } @@ -652,8 +650,6 @@ bool Climate::set_custom_preset_(const char *preset) { this->has_custom_preset()); } -bool Climate::set_custom_preset_(const std::string &preset) { return this->set_custom_preset_(preset.c_str()); } - void Climate::clear_custom_preset_() { this->custom_preset_ = nullptr; } const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) { diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 8e2bd67995..c36625b2ae 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -272,8 +272,6 @@ class Climate : public EntityBase { /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed. bool set_custom_fan_mode_(const char *mode); - /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed. - bool set_custom_fan_mode_(const std::string &mode); /// Clear custom fan mode. void clear_custom_fan_mode_(); @@ -282,8 +280,6 @@ class Climate : public EntityBase { /// Set custom preset. Reset primary preset. Return true if preset has been changed. bool set_custom_preset_(const char *preset); - /// Set custom preset. Reset primary preset. Return true if preset has been changed. - bool set_custom_preset_(const std::string &preset); /// Clear custom preset. void clear_custom_preset_(); diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 2c8e3e4d9a..5e52c4721e 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -224,7 +224,7 @@ void ThermostatClimate::control(const climate::ClimateCall &call) { this->change_custom_preset_(call.get_custom_preset().value()); } else { // Use the base class method which handles pointer lookup internally - this->set_custom_preset_(call.get_custom_preset().value()); + this->set_custom_preset_(call.get_custom_preset().value().c_str()); } } @@ -1189,7 +1189,7 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; // Use the base class method which handles pointer lookup and preset reset internally - this->set_custom_preset_(custom_preset); + this->set_custom_preset_(custom_preset.c_str()); if (trig != nullptr) { trig->trigger(); } From 1fd6f7bcd32ad5ceee96ccb90868e2a6b6e568f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:41:44 -0500 Subject: [PATCH 322/394] simplify --- esphome/components/bedjet/climate/bedjet_climate.cpp | 8 ++------ esphome/components/demo/demo_climate.h | 2 +- esphome/components/thermostat/thermostat_climate.cpp | 4 +--- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/esphome/components/bedjet/climate/bedjet_climate.cpp b/esphome/components/bedjet/climate/bedjet_climate.cpp index 737000f9ae..52cc76f147 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.cpp +++ b/esphome/components/bedjet/climate/bedjet_climate.cpp @@ -144,8 +144,7 @@ void BedJetClimate::control(const ClimateCall &call) { if (result) { this->mode = CLIMATE_MODE_HEAT; - this->preset = CLIMATE_PRESET_BOOST; - this->clear_custom_preset_(); + this->set_preset_(CLIMATE_PRESET_BOOST); } } else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) { if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) { @@ -259,7 +258,6 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { case MODE_HEAT: this->mode = CLIMATE_MODE_HEAT; this->action = CLIMATE_ACTION_HEATING; - this->preset.reset(); if (this->heating_mode_ == HEAT_MODE_EXTENDED) { this->set_custom_preset_("LTD HT"); } else { @@ -270,7 +268,6 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { case MODE_EXTHT: this->mode = CLIMATE_MODE_HEAT; this->action = CLIMATE_ACTION_HEATING; - this->preset.reset(); if (this->heating_mode_ == HEAT_MODE_EXTENDED) { this->clear_custom_preset_(); } else { @@ -293,8 +290,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { break; case MODE_TURBO: - this->preset = CLIMATE_PRESET_BOOST; - this->clear_custom_preset_(); + this->set_preset_(CLIMATE_PRESET_BOOST); this->mode = CLIMATE_MODE_HEAT; this->action = CLIMATE_ACTION_HEATING; break; diff --git a/esphome/components/demo/demo_climate.h b/esphome/components/demo/demo_climate.h index 0a71ec6dab..f8944b0735 100644 --- a/esphome/components/demo/demo_climate.h +++ b/esphome/components/demo/demo_climate.h @@ -37,7 +37,7 @@ class DemoClimate : public climate::Climate, public Component { this->mode = climate::CLIMATE_MODE_HEAT_COOL; this->set_custom_fan_mode_("Auto Low"); this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; - this->preset = climate::CLIMATE_PRESET_AWAY; + this->set_preset_(climate::CLIMATE_PRESET_AWAY); break; } this->publish_state(); diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 5e52c4721e..d2f5db3b32 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -1162,7 +1162,7 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { this->preset.value() != preset) { // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; - this->preset = preset; + this->set_preset_(preset); if (trig != nullptr) { trig->trigger(); } @@ -1172,8 +1172,6 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { } else { ESP_LOGI(TAG, "No changes required to apply preset %s", LOG_STR_ARG(climate::climate_preset_to_string(preset))); } - this->clear_custom_preset_(); - this->preset = preset; } else { ESP_LOGW(TAG, "Preset %s not configured; ignoring", LOG_STR_ARG(climate::climate_preset_to_string(preset))); } From f6e8fdcd9149d22d76b00dcb05c363f43dbd8d2e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:50:00 -0500 Subject: [PATCH 323/394] simplify --- esphome/components/api/api_connection.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 7413b0c419..a0e0638860 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -699,11 +699,11 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { if (msg.has_fan_mode) call.set_fan_mode(static_cast(msg.fan_mode)); if (msg.has_custom_fan_mode) - call.set_fan_mode(msg.custom_fan_mode.c_str()); + call.set_fan_mode(msg.custom_fan_mode); if (msg.has_preset) call.set_preset(static_cast(msg.preset)); if (msg.has_custom_preset) - call.set_preset(msg.custom_preset.c_str()); + call.set_preset(msg.custom_preset); if (msg.has_swing_mode) call.set_swing_mode(static_cast(msg.swing_mode)); call.perform(); From d7f55e9977c7240e5436967caf0c27c7ae3445f4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:53:30 -0500 Subject: [PATCH 324/394] fixes --- esphome/components/bedjet/climate/bedjet_climate.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/bedjet/climate/bedjet_climate.cpp b/esphome/components/bedjet/climate/bedjet_climate.cpp index 52cc76f147..877fd6f771 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.cpp +++ b/esphome/components/bedjet/climate/bedjet_climate.cpp @@ -258,6 +258,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { case MODE_HEAT: this->mode = CLIMATE_MODE_HEAT; this->action = CLIMATE_ACTION_HEATING; + this->preset.reset(); if (this->heating_mode_ == HEAT_MODE_EXTENDED) { this->set_custom_preset_("LTD HT"); } else { @@ -268,6 +269,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { case MODE_EXTHT: this->mode = CLIMATE_MODE_HEAT; this->action = CLIMATE_ACTION_HEATING; + this->preset.reset(); if (this->heating_mode_ == HEAT_MODE_EXTENDED) { this->clear_custom_preset_(); } else { From 1b5a942f6160c711efc8b4f1846f0f045506e899 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 20:58:02 -0500 Subject: [PATCH 325/394] fixes --- esphome/components/thermostat/thermostat_climate.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index d2f5db3b32..8258fa9d65 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -1197,6 +1197,9 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) } else { ESP_LOGI(TAG, "No changes required to apply custom preset %s", custom_preset.c_str()); } + // Note: set_custom_preset_() above handles preset.reset() and custom_preset_ assignment internally. + // The old code had these lines here unconditionally, which was a bug (double assignment, state modification + // even when no changes were needed). Now properly handled by the protected setter with mutual exclusion. } else { ESP_LOGW(TAG, "Custom preset %s not configured; ignoring", custom_preset.c_str()); } From c36b7781589d139a420b9deda02cbebfc2ee629b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 21:07:23 -0500 Subject: [PATCH 326/394] safety --- esphome/components/climate/climate.cpp | 46 ++++++++++++++++++--- esphome/components/climate/climate.h | 30 +++++++++++++- esphome/components/climate/climate_traits.h | 46 +++++++++++++++++++-- 3 files changed, 112 insertions(+), 10 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index ebc9e466e0..e596582de8 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -593,8 +593,25 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { climate->publish_state(); } -// Template helper for setting primary modes with mutual exclusion -// Clears custom pointer and sets primary optional value +/** Template helper for setting primary modes (fan_mode, preset) with mutual exclusion. + * + * Climate devices have mutually exclusive mode pairs: + * - fan_mode (enum) vs custom_fan_mode_ (const char*) + * - preset (enum) vs custom_preset_ (const char*) + * + * Only one mode in each pair can be active at a time. This helper ensures setting a primary + * mode automatically clears its corresponding custom mode. + * + * Example state transitions: + * Before: custom_fan_mode_="Turbo", fan_mode=nullopt + * Call: set_fan_mode_(CLIMATE_FAN_HIGH) + * After: custom_fan_mode_=nullptr, fan_mode=CLIMATE_FAN_HIGH + * + * @param primary The primary mode optional (fan_mode or preset) + * @param custom_ptr Reference to the custom mode pointer (custom_fan_mode_ or custom_preset_) + * @param value The new primary mode value to set + * @return true if state changed, false if already set to this value + */ template bool set_primary_mode(optional &primary, const char *&custom_ptr, T value) { // Clear the custom mode (mutual exclusion) bool changed = custom_ptr != nullptr; @@ -607,15 +624,34 @@ template bool set_primary_mode(optional &primary, const char *&cu return false; } -// Template helper for setting custom modes with mutual exclusion -// Takes pre-computed values: the found pointer from traits and whether custom mode is currently set +/** Template helper for setting custom modes (custom_fan_mode_, custom_preset_) with mutual exclusion. + * + * This helper ensures setting a custom mode automatically clears its corresponding primary mode. + * It also validates that the custom mode exists in the device's supported modes (lifetime safety). + * + * Example state transitions: + * Before: fan_mode=CLIMATE_FAN_HIGH, custom_fan_mode_=nullptr + * Call: set_custom_fan_mode_("Turbo") + * After: fan_mode=nullopt, custom_fan_mode_="Turbo" (pointer from traits) + * + * Lifetime Safety: + * - found_ptr must come from traits.find_custom_*_mode_() + * - Only pointers found in traits are stored, ensuring they remain valid + * - Prevents dangling pointers from temporary strings + * + * @param custom_ptr Reference to the custom mode pointer to set + * @param primary The primary mode optional to clear + * @param found_ptr The validated pointer from traits (nullptr if not found) + * @param has_custom Whether a custom mode is currently active + * @return true if state changed, false otherwise + */ template bool set_custom_mode(const char *&custom_ptr, optional &primary, const char *found_ptr, bool has_custom) { if (found_ptr != nullptr) { // Clear the primary mode (mutual exclusion) bool changed = primary.has_value(); primary.reset(); - // Set the custom mode + // Set the custom mode (pointer is validated by caller from traits) if (changed || custom_ptr != found_ptr) { custom_ptr = found_ptr; return true; diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index c36625b2ae..c6cd7005c5 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -325,10 +325,36 @@ class Climate : public EntityBase { optional visual_min_humidity_override_{}; optional visual_max_humidity_override_{}; - /// The active custom fan mode of the climate device (protected - use get_custom_fan_mode() or setters). + /** The active custom fan mode of the climate device. + * + * PROTECTED ACCESS: External components must use get_custom_fan_mode() for read access. + * Derived climate classes must use set_custom_fan_mode_() / clear_custom_fan_mode_() to modify. + * + * POINTER LIFETIME SAFETY: + * This pointer MUST always point to an entry in the traits.supported_custom_fan_modes_ vector, + * or be nullptr. The protected setter set_custom_fan_mode_() enforces this by calling + * traits.find_custom_fan_mode_() to validate and obtain the correct pointer. + * + * Never assign directly - always use setters: + * this->set_custom_fan_mode_("Turbo"); // ✓ Safe - validates against traits + * this->custom_fan_mode_ = "Turbo"; // ✗ UNSAFE - may create dangling pointer + */ const char *custom_fan_mode_{nullptr}; - /// The active custom preset mode of the climate device (protected - use get_custom_preset() or setters). + /** The active custom preset mode of the climate device. + * + * PROTECTED ACCESS: External components must use get_custom_preset() for read access. + * Derived climate classes must use set_custom_preset_() / clear_custom_preset_() to modify. + * + * POINTER LIFETIME SAFETY: + * This pointer MUST always point to an entry in the traits.supported_custom_presets_ vector, + * or be nullptr. The protected setter set_custom_preset_() enforces this by calling + * traits.find_custom_preset_() to validate and obtain the correct pointer. + * + * Never assign directly - always use setters: + * this->set_custom_preset_("Eco"); // ✓ Safe - validates against traits + * this->custom_preset_ = "Eco"; // ✗ UNSAFE - may create dangling pointer + */ const char *custom_preset_{nullptr}; }; diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index cbd9d1dbf4..14dcbcff6c 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -157,6 +157,11 @@ class ClimateTraits { template void set_supported_custom_fan_modes(const char *const (&modes)[N]) { this->supported_custom_fan_modes_.assign(modes, modes + N); } + + // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages + void set_supported_custom_fan_modes(const std::vector &modes) = delete; + void set_supported_custom_fan_modes(std::initializer_list modes) = delete; + const std::vector &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } bool supports_custom_fan_mode(const char *custom_fan_mode) const { return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode); @@ -180,6 +185,11 @@ class ClimateTraits { template void set_supported_custom_presets(const char *const (&presets)[N]) { this->supported_custom_presets_.assign(presets, presets + N); } + + // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages + void set_supported_custom_presets(const std::vector &presets) = delete; + void set_supported_custom_presets(std::initializer_list presets) = delete; + const std::vector &get_supported_custom_presets() const { return this->supported_custom_presets_; } bool supports_custom_preset(const char *custom_preset) const { return vector_contains(this->supported_custom_presets_, custom_preset); @@ -269,9 +279,39 @@ class ClimateTraits { climate::ClimateFanModeMask supported_fan_modes_; climate::ClimateSwingModeMask supported_swing_modes_; climate::ClimatePresetMask supported_presets_; - // Store const char* pointers to avoid std::string overhead - // Pointers must remain valid for traits lifetime (typically string literals in rodata, - // or pointers to strings with sufficient lifetime like member variables) + + /** Custom mode storage using const char* pointers to eliminate std::string overhead. + * + * POINTER LIFETIME SAFETY REQUIREMENTS: + * Pointers stored here MUST remain valid for the entire lifetime of the ClimateTraits object. + * This is guaranteed when pointers point to: + * + * 1. String literals (rodata section, valid for program lifetime): + * traits.set_supported_custom_fan_modes({"Turbo", "Silent"}); + * + * 2. Static const data (valid for program lifetime): + * static const char* PRESET_ECO = "Eco"; + * traits.set_supported_custom_presets({PRESET_ECO}); + * + * 3. Member variables with sufficient lifetime: + * class MyClimate { + * std::vector custom_presets_; // Lives as long as component + * ClimateTraits traits() { + * // Extract from map keys that live as long as the component + * for (const auto& [name, config] : preset_map_) { + * custom_presets_.push_back(name.c_str()); + * } + * traits.set_supported_custom_presets(custom_presets_); + * } + * }; + * + * UNSAFE PATTERNS TO AVOID: + * std::string temp = "Mode"; + * traits.set_supported_custom_fan_modes({temp.c_str()}); // DANGLING POINTER! + * + * Protected setters in Climate class automatically validate pointers against these + * vectors, ensuring only safe pointers are stored in device state. + */ std::vector supported_custom_fan_modes_; std::vector supported_custom_presets_; }; From 868d01ae039657eb5ca4fe89997cfd635ccb620e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 21:10:01 -0500 Subject: [PATCH 327/394] safety --- esphome/components/climate/climate.h | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index c6cd7005c5..050fc5e475 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -325,35 +325,40 @@ class Climate : public EntityBase { optional visual_min_humidity_override_{}; optional visual_max_humidity_override_{}; + private: /** The active custom fan mode of the climate device. * - * PROTECTED ACCESS: External components must use get_custom_fan_mode() for read access. - * Derived climate classes must use set_custom_fan_mode_() / clear_custom_fan_mode_() to modify. + * PRIVATE ACCESS (compile-time enforced safety): + * - External components: Use get_custom_fan_mode() for read-only access + * - Derived classes: Use set_custom_fan_mode_() / clear_custom_fan_mode_() to modify + * - Direct assignment is prevented at compile time * * POINTER LIFETIME SAFETY: * This pointer MUST always point to an entry in the traits.supported_custom_fan_modes_ vector, * or be nullptr. The protected setter set_custom_fan_mode_() enforces this by calling * traits.find_custom_fan_mode_() to validate and obtain the correct pointer. * - * Never assign directly - always use setters: + * The private access level provides compile-time enforcement: * this->set_custom_fan_mode_("Turbo"); // ✓ Safe - validates against traits - * this->custom_fan_mode_ = "Turbo"; // ✗ UNSAFE - may create dangling pointer + * this->custom_fan_mode_ = "Turbo"; // ✗ Compile error - private member */ const char *custom_fan_mode_{nullptr}; /** The active custom preset mode of the climate device. * - * PROTECTED ACCESS: External components must use get_custom_preset() for read access. - * Derived climate classes must use set_custom_preset_() / clear_custom_preset_() to modify. + * PRIVATE ACCESS (compile-time enforced safety): + * - External components: Use get_custom_preset() for read-only access + * - Derived classes: Use set_custom_preset_() / clear_custom_preset_() to modify + * - Direct assignment is prevented at compile time * * POINTER LIFETIME SAFETY: * This pointer MUST always point to an entry in the traits.supported_custom_presets_ vector, * or be nullptr. The protected setter set_custom_preset_() enforces this by calling * traits.find_custom_preset_() to validate and obtain the correct pointer. * - * Never assign directly - always use setters: + * The private access level provides compile-time enforcement: * this->set_custom_preset_("Eco"); // ✓ Safe - validates against traits - * this->custom_preset_ = "Eco"; // ✗ UNSAFE - may create dangling pointer + * this->custom_preset_ = "Eco"; // ✗ Compile error - private member */ const char *custom_preset_{nullptr}; }; From 1378e52838f1d1c1ae02a9473694d91cfb8815ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 21:10:19 -0500 Subject: [PATCH 328/394] safety --- esphome/components/climate/climate.h | 34 +++++----------------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 050fc5e475..091483a033 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -326,39 +326,17 @@ class Climate : public EntityBase { optional visual_max_humidity_override_{}; private: - /** The active custom fan mode of the climate device. + /** The active custom fan mode (private - enforces use of safe setters). * - * PRIVATE ACCESS (compile-time enforced safety): - * - External components: Use get_custom_fan_mode() for read-only access - * - Derived classes: Use set_custom_fan_mode_() / clear_custom_fan_mode_() to modify - * - Direct assignment is prevented at compile time - * - * POINTER LIFETIME SAFETY: - * This pointer MUST always point to an entry in the traits.supported_custom_fan_modes_ vector, - * or be nullptr. The protected setter set_custom_fan_mode_() enforces this by calling - * traits.find_custom_fan_mode_() to validate and obtain the correct pointer. - * - * The private access level provides compile-time enforcement: - * this->set_custom_fan_mode_("Turbo"); // ✓ Safe - validates against traits - * this->custom_fan_mode_ = "Turbo"; // ✗ Compile error - private member + * Points to an entry in traits.supported_custom_fan_modes_ or nullptr. + * Use get_custom_fan_mode() to read, set_custom_fan_mode_() to modify. */ const char *custom_fan_mode_{nullptr}; - /** The active custom preset mode of the climate device. + /** The active custom preset (private - enforces use of safe setters). * - * PRIVATE ACCESS (compile-time enforced safety): - * - External components: Use get_custom_preset() for read-only access - * - Derived classes: Use set_custom_preset_() / clear_custom_preset_() to modify - * - Direct assignment is prevented at compile time - * - * POINTER LIFETIME SAFETY: - * This pointer MUST always point to an entry in the traits.supported_custom_presets_ vector, - * or be nullptr. The protected setter set_custom_preset_() enforces this by calling - * traits.find_custom_preset_() to validate and obtain the correct pointer. - * - * The private access level provides compile-time enforcement: - * this->set_custom_preset_("Eco"); // ✓ Safe - validates against traits - * this->custom_preset_ = "Eco"; // ✗ Compile error - private member + * Points to an entry in traits.supported_custom_presets_ or nullptr. + * Use get_custom_preset() to read, set_custom_preset_() to modify. */ const char *custom_preset_{nullptr}; }; From 5c99eabd1a8db566b812fbd5e89bb36fb740ce4a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 21:11:33 -0500 Subject: [PATCH 329/394] safety --- esphome/components/climate/climate_traits.h | 33 ++++----------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 14dcbcff6c..fff1144620 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -282,35 +282,12 @@ class ClimateTraits { /** Custom mode storage using const char* pointers to eliminate std::string overhead. * - * POINTER LIFETIME SAFETY REQUIREMENTS: - * Pointers stored here MUST remain valid for the entire lifetime of the ClimateTraits object. - * This is guaranteed when pointers point to: + * Pointers must remain valid for the ClimateTraits lifetime. Safe patterns: + * - String literals: set_supported_custom_fan_modes({"Turbo", "Silent"}) + * - Static data: static const char* MODE = "Eco"; + * - Component members: Extract from long-lived std::map keys or member vectors * - * 1. String literals (rodata section, valid for program lifetime): - * traits.set_supported_custom_fan_modes({"Turbo", "Silent"}); - * - * 2. Static const data (valid for program lifetime): - * static const char* PRESET_ECO = "Eco"; - * traits.set_supported_custom_presets({PRESET_ECO}); - * - * 3. Member variables with sufficient lifetime: - * class MyClimate { - * std::vector custom_presets_; // Lives as long as component - * ClimateTraits traits() { - * // Extract from map keys that live as long as the component - * for (const auto& [name, config] : preset_map_) { - * custom_presets_.push_back(name.c_str()); - * } - * traits.set_supported_custom_presets(custom_presets_); - * } - * }; - * - * UNSAFE PATTERNS TO AVOID: - * std::string temp = "Mode"; - * traits.set_supported_custom_fan_modes({temp.c_str()}); // DANGLING POINTER! - * - * Protected setters in Climate class automatically validate pointers against these - * vectors, ensuring only safe pointers are stored in device state. + * Climate class setters validate pointers are from these vectors before storing. */ std::vector supported_custom_fan_modes_; std::vector supported_custom_presets_; From fae90194e7c0461579adadf1392dce6b8b2a3b41 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Oct 2025 21:12:27 -0500 Subject: [PATCH 330/394] safety --- esphome/components/climate/climate_traits.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index fff1144620..0eecf9789f 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -284,8 +284,7 @@ class ClimateTraits { * * Pointers must remain valid for the ClimateTraits lifetime. Safe patterns: * - String literals: set_supported_custom_fan_modes({"Turbo", "Silent"}) - * - Static data: static const char* MODE = "Eco"; - * - Component members: Extract from long-lived std::map keys or member vectors + * - Static const data: static const char* MODE = "Eco"; * * Climate class setters validate pointers are from these vectors before storing. */ From cd3f10630b5280e5ab9cf2dd743be50cf39af5a6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 31 Oct 2025 11:01:36 -0500 Subject: [PATCH 331/394] wip --- esphome/components/copy/fan/copy_fan.cpp | 14 ++- esphome/components/fan/fan.cpp | 88 +++++++++++++------ esphome/components/fan/fan.h | 28 ++++-- esphome/components/fan/fan_traits.h | 12 +++ .../components/hbridge/fan/hbridge_fan.cpp | 8 +- esphome/components/speed/fan/speed_fan.cpp | 8 +- .../components/template/fan/template_fan.cpp | 8 +- 7 files changed, 125 insertions(+), 41 deletions(-) diff --git a/esphome/components/copy/fan/copy_fan.cpp b/esphome/components/copy/fan/copy_fan.cpp index 15a7f5e025..c1e873e083 100644 --- a/esphome/components/copy/fan/copy_fan.cpp +++ b/esphome/components/copy/fan/copy_fan.cpp @@ -12,7 +12,11 @@ void CopyFan::setup() { this->oscillating = source_->oscillating; this->speed = source_->speed; this->direction = source_->direction; - this->preset_mode = source_->preset_mode; + const char *preset = source_->get_preset_mode(); + if (preset != nullptr) + this->set_preset_mode_(preset); + else + this->clear_preset_mode_(); this->publish_state(); }); @@ -20,7 +24,11 @@ void CopyFan::setup() { this->oscillating = source_->oscillating; this->speed = source_->speed; this->direction = source_->direction; - this->preset_mode = source_->preset_mode; + const char *preset = source_->get_preset_mode(); + if (preset != nullptr) + this->set_preset_mode_(preset); + else + this->clear_preset_mode_(); this->publish_state(); } @@ -49,7 +57,7 @@ void CopyFan::control(const fan::FanCall &call) { call2.set_speed(*call.get_speed()); if (call.get_direction().has_value()) call2.set_direction(*call.get_direction()); - if (!call.get_preset_mode().empty()) + if (call.has_preset_mode()) call2.set_preset_mode(call.get_preset_mode()); call2.perform(); } diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 5b4f437f99..e38b7b43a4 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -17,6 +17,27 @@ const LogString *fan_direction_to_string(FanDirection direction) { } } +FanCall &FanCall::set_preset_mode(const std::string &preset_mode) { return this->set_preset_mode(preset_mode.c_str()); } + +FanCall &FanCall::set_preset_mode(const char *preset_mode) { + if (preset_mode == nullptr || strlen(preset_mode) == 0) { + this->preset_mode_ = nullptr; + return *this; + } + + // Find and validate pointer from traits immediately + auto traits = this->parent_.get_traits(); + const char *validated_mode = traits.find_preset_mode(preset_mode); + if (validated_mode != nullptr) { + this->preset_mode_ = validated_mode; // Store pointer from traits + } else { + // Preset mode not found in traits - log warning and don't set + ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), preset_mode); + this->preset_mode_ = nullptr; + } + return *this; +} + void FanCall::perform() { ESP_LOGD(TAG, "'%s' - Setting:", this->parent_.get_name().c_str()); this->validate_(); @@ -32,8 +53,8 @@ void FanCall::perform() { if (this->direction_.has_value()) { ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_))); } - if (!this->preset_mode_.empty()) { - ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_.c_str()); + if (this->has_preset_mode()) { + ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_); } this->parent_.control(*this); } @@ -46,30 +67,15 @@ void FanCall::validate_() { // https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes // "Manually setting a speed must disable any set preset mode" - this->preset_mode_.clear(); - } - - if (!this->preset_mode_.empty()) { - const auto &preset_modes = traits.supported_preset_modes(); - bool found = false; - for (const auto &mode : preset_modes) { - if (strcmp(mode, this->preset_mode_.c_str()) == 0) { - found = true; - break; - } - } - if (!found) { - ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), this->preset_mode_.c_str()); - this->preset_mode_.clear(); - } + this->preset_mode_ = nullptr; } // when turning on... if (!this->parent_.state && this->binary_state_.has_value() && *this->binary_state_ // ..,and no preset mode will be active... - && this->preset_mode_.empty() && - this->parent_.preset_mode.empty() + && !this->has_preset_mode() && + this->parent_.get_preset_mode() == nullptr // ...and neither current nor new speed is available... && traits.supports_speed() && this->parent_.speed == 0 && !this->speed_.has_value()) { // ...set speed to 100% @@ -120,7 +126,7 @@ void FanRestoreState::apply(Fan &fan) { // Use stored preset index to get preset name const auto &preset_modes = traits.supported_preset_modes(); if (this->preset_mode < preset_modes.size()) { - fan.preset_mode = preset_modes[this->preset_mode]; + fan.set_preset_mode_(preset_modes[this->preset_mode]); } } fan.publish_state(); @@ -131,6 +137,36 @@ FanCall Fan::turn_off() { return this->make_call().set_state(false); } FanCall Fan::toggle() { return this->make_call().set_state(!this->state); } FanCall Fan::make_call() { return FanCall(*this); } +const char *Fan::find_preset_mode_(const char *preset_mode) { return this->get_traits().find_preset_mode(preset_mode); } + +bool Fan::set_preset_mode_(const char *preset_mode) { + const char *validated = this->find_preset_mode_(preset_mode); + if (validated == nullptr) { + return false; // Preset mode not supported + } + if (this->preset_mode_ == validated) { + return false; // No change + } + this->preset_mode_ = validated; + // Keep deprecated member in sync during deprecation period +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + this->preset_mode = validated; +#pragma GCC diagnostic pop + return true; +} + +bool Fan::set_preset_mode_(const std::string &preset_mode) { return this->set_preset_mode_(preset_mode.c_str()); } + +void Fan::clear_preset_mode_() { + this->preset_mode_ = nullptr; + // Keep deprecated member in sync during deprecation period +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + this->preset_mode.clear(); +#pragma GCC diagnostic pop +} + void Fan::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } void Fan::publish_state() { auto traits = this->get_traits(); @@ -146,8 +182,9 @@ void Fan::publish_state() { if (traits.supports_direction()) { ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction))); } - if (traits.supports_preset_modes() && !this->preset_mode.empty()) { - ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode.c_str()); + const char *preset = this->get_preset_mode(); + if (traits.supports_preset_modes() && preset != nullptr) { + ESP_LOGD(TAG, " Preset Mode: %s", preset); } this->state_callback_.call(); this->save_state_(); @@ -199,12 +236,13 @@ void Fan::save_state_() { state.speed = this->speed; state.direction = this->direction; - if (traits.supports_preset_modes() && !this->preset_mode.empty()) { + const char *preset = this->get_preset_mode(); + if (traits.supports_preset_modes() && preset != nullptr) { const auto &preset_modes = traits.supported_preset_modes(); // Store index of current preset mode size_t i = 0; for (const auto &mode : preset_modes) { - if (strcmp(mode, this->preset_mode.c_str()) == 0) { + if (strcmp(mode, preset) == 0) { state.preset_mode = i; break; } diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index 3739de29a2..5bbddb0005 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -70,11 +70,10 @@ class FanCall { return *this; } optional get_direction() const { return this->direction_; } - FanCall &set_preset_mode(const std::string &preset_mode) { - this->preset_mode_ = preset_mode; - return *this; - } - std::string get_preset_mode() const { return this->preset_mode_; } + FanCall &set_preset_mode(const std::string &preset_mode); + FanCall &set_preset_mode(const char *preset_mode); + const char *get_preset_mode() const { return this->preset_mode_; } + bool has_preset_mode() const { return this->preset_mode_ != nullptr; } void perform(); @@ -86,7 +85,7 @@ class FanCall { optional oscillating_; optional speed_; optional direction_{}; - std::string preset_mode_{}; + const char *preset_mode_{nullptr}; // Pointer to string in traits (after validation) }; struct FanRestoreState { @@ -113,7 +112,9 @@ class Fan : public EntityBase { /// The current direction of the fan FanDirection direction{FanDirection::FORWARD}; // The current preset mode of the fan - std::string preset_mode{}; + // Deprecated: Use get_preset_mode() instead. Will be removed in 2026.5.0 + std::string preset_mode {} + __attribute__((deprecated("Use get_preset_mode() instead of .preset_mode. Will be removed in 2026.5.0"))); FanCall turn_on(); FanCall turn_off(); @@ -130,6 +131,9 @@ class Fan : public EntityBase { /// Set the restore mode of this fan. void set_restore_mode(FanRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } + /// Get the current preset mode (returns pointer to string stored in traits, or nullptr if not set) + const char *get_preset_mode() const { return this->preset_mode_; } + protected: friend FanCall; @@ -140,9 +144,19 @@ class Fan : public EntityBase { void dump_traits_(const char *tag, const char *prefix); + /// Set the preset mode (finds and stores pointer from traits). Returns true if changed. + bool set_preset_mode_(const char *preset_mode); + /// Set the preset mode (finds and stores pointer from traits). Returns true if changed. + bool set_preset_mode_(const std::string &preset_mode); + /// Clear the preset mode + void clear_preset_mode_(); + /// Find and return the matching preset mode pointer from traits, or nullptr if not found. + const char *find_preset_mode_(const char *preset_mode); + CallbackManager state_callback_{}; ESPPreferenceObject rtc_; FanRestoreMode restore_mode_; + const char *preset_mode_{nullptr}; }; } // namespace fan diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index bfb17a05ab..eb6f726a3c 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -39,6 +40,17 @@ class FanTraits { void set_supported_preset_modes(const std::vector &preset_modes) { this->preset_modes_ = preset_modes; } /// Return if preset modes are supported bool supports_preset_modes() const { return !this->preset_modes_.empty(); } + /// Find and return the matching preset mode pointer from supported modes, or nullptr if not found. + const char *find_preset_mode(const char *preset_mode) const { + if (preset_mode == nullptr) + return nullptr; + for (const char *mode : this->preset_modes_) { + if (strcmp(mode, preset_mode) == 0) { + return mode; // Return pointer from traits + } + } + return nullptr; + } protected: bool oscillation_{false}; diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 605a9d4ef3..01680ae651 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -51,13 +51,17 @@ void HBridgeFan::dump_config() { void HBridgeFan::control(const fan::FanCall &call) { if (call.get_state().has_value()) this->state = *call.get_state(); - if (call.get_speed().has_value()) + if (call.get_speed().has_value()) { this->speed = *call.get_speed(); + // Speed manually set, clear preset mode + this->clear_preset_mode_(); + } if (call.get_oscillating().has_value()) this->oscillating = *call.get_oscillating(); if (call.get_direction().has_value()) this->direction = *call.get_direction(); - this->preset_mode = call.get_preset_mode(); + if (call.has_preset_mode()) + this->set_preset_mode_(call.get_preset_mode()); this->write_state_(); this->publish_state(); diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 57bd795416..43b149e382 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -23,13 +23,17 @@ void SpeedFan::dump_config() { LOG_FAN("", "Speed Fan", this); } void SpeedFan::control(const fan::FanCall &call) { if (call.get_state().has_value()) this->state = *call.get_state(); - if (call.get_speed().has_value()) + if (call.get_speed().has_value()) { this->speed = *call.get_speed(); + // Speed manually set, clear preset mode + this->clear_preset_mode_(); + } if (call.get_oscillating().has_value()) this->oscillating = *call.get_oscillating(); if (call.get_direction().has_value()) this->direction = *call.get_direction(); - this->preset_mode = call.get_preset_mode(); + if (call.has_preset_mode()) + this->set_preset_mode_(call.get_preset_mode()); this->write_state_(); this->publish_state(); diff --git a/esphome/components/template/fan/template_fan.cpp b/esphome/components/template/fan/template_fan.cpp index 5f4a2ae8f7..4ec7e121cf 100644 --- a/esphome/components/template/fan/template_fan.cpp +++ b/esphome/components/template/fan/template_fan.cpp @@ -23,13 +23,17 @@ void TemplateFan::dump_config() { LOG_FAN("", "Template Fan", this); } void TemplateFan::control(const fan::FanCall &call) { if (call.get_state().has_value()) this->state = *call.get_state(); - if (call.get_speed().has_value() && (this->speed_count_ > 0)) + if (call.get_speed().has_value() && (this->speed_count_ > 0)) { this->speed = *call.get_speed(); + // Speed manually set, clear preset mode + this->clear_preset_mode_(); + } if (call.get_oscillating().has_value() && this->has_oscillating_) this->oscillating = *call.get_oscillating(); if (call.get_direction().has_value() && this->has_direction_) this->direction = *call.get_direction(); - this->preset_mode = call.get_preset_mode(); + if (call.has_preset_mode()) + this->set_preset_mode_(call.get_preset_mode()); this->publish_state(); } From 58ae4a38be7b4905ce490ffe722b12de6bb9ffd7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 31 Oct 2025 11:04:27 -0500 Subject: [PATCH 332/394] wip --- esphome/components/fan/fan.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index 5bbddb0005..1ca7bfbeb3 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -113,8 +113,8 @@ class Fan : public EntityBase { FanDirection direction{FanDirection::FORWARD}; // The current preset mode of the fan // Deprecated: Use get_preset_mode() instead. Will be removed in 2026.5.0 - std::string preset_mode {} __attribute__((deprecated("Use get_preset_mode() instead of .preset_mode. Will be removed in 2026.5.0"))); + std::string preset_mode{}; FanCall turn_on(); FanCall turn_off(); From cf85621d64e331d0792b47441bc5bd9a71a86873 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 31 Oct 2025 11:05:31 -0500 Subject: [PATCH 333/394] wip --- esphome/components/fan/fan.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index 1ca7bfbeb3..6cae01999d 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -136,6 +136,7 @@ class Fan : public EntityBase { protected: friend FanCall; + friend struct FanRestoreState; virtual void control(const FanCall &call) = 0; From 79e2340588fdf6c538b32d71360ee162017f0229 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 31 Oct 2025 11:06:18 -0500 Subject: [PATCH 334/394] wip --- esphome/components/fan/fan.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index e38b7b43a4..24ce9188f4 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -121,14 +121,12 @@ void FanRestoreState::apply(Fan &fan) { fan.speed = this->speed; fan.direction = this->direction; - auto traits = fan.get_traits(); - if (traits.supports_preset_modes()) { - // Use stored preset index to get preset name - const auto &preset_modes = traits.supported_preset_modes(); - if (this->preset_mode < preset_modes.size()) { - fan.set_preset_mode_(preset_modes[this->preset_mode]); - } + // Use stored preset index to get preset name from traits + const auto &preset_modes = fan.get_traits().supported_preset_modes(); + if (this->preset_mode < preset_modes.size()) { + fan.set_preset_mode_(preset_modes[this->preset_mode]); } + fan.publish_state(); } From 4fabe464c8b193b29d652d1012f6f01a111985b9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 31 Oct 2025 11:08:24 -0500 Subject: [PATCH 335/394] wip --- esphome/components/fan/fan.cpp | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 24ce9188f4..eb2e8743f3 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -139,11 +139,8 @@ const char *Fan::find_preset_mode_(const char *preset_mode) { return this->get_t bool Fan::set_preset_mode_(const char *preset_mode) { const char *validated = this->find_preset_mode_(preset_mode); - if (validated == nullptr) { - return false; // Preset mode not supported - } - if (this->preset_mode_ == validated) { - return false; // No change + if (validated == nullptr || this->preset_mode_ == validated) { + return false; // Preset mode not supported or no change } this->preset_mode_ = validated; // Keep deprecated member in sync during deprecation period @@ -235,16 +232,14 @@ void Fan::save_state_() { state.direction = this->direction; const char *preset = this->get_preset_mode(); - if (traits.supports_preset_modes() && preset != nullptr) { + if (preset != nullptr) { const auto &preset_modes = traits.supported_preset_modes(); - // Store index of current preset mode - size_t i = 0; - for (const auto &mode : preset_modes) { - if (strcmp(mode, preset) == 0) { + // Find index of current preset mode (pointer comparison is safe since preset is from traits) + for (size_t i = 0; i < preset_modes.size(); i++) { + if (preset_modes[i] == preset) { state.preset_mode = i; break; } - i++; } } From 410afd196f05c85050c21f751e19e5598f51bd15 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 31 Oct 2025 11:13:57 -0500 Subject: [PATCH 336/394] preen --- esphome/components/fan/fan_traits.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index bfb17a05ab..df345f9b04 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -37,6 +37,11 @@ class FanTraits { } /// Set the preset modes supported by the fan (from vector). void set_supported_preset_modes(const std::vector &preset_modes) { this->preset_modes_ = preset_modes; } + + // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages + void set_supported_preset_modes(const std::vector &preset_modes) = delete; + void set_supported_preset_modes(std::initializer_list preset_modes) = delete; + /// Return if preset modes are supported bool supports_preset_modes() const { return !this->preset_modes_.empty(); } From 91ae8c82b0ddd655aa88197dd455cea1b26995bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 31 Oct 2025 11:15:59 -0500 Subject: [PATCH 337/394] preen --- esphome/components/fan/fan.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index 6cae01999d..16fe42f9f4 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -112,8 +112,9 @@ class Fan : public EntityBase { /// The current direction of the fan FanDirection direction{FanDirection::FORWARD}; // The current preset mode of the fan - // Deprecated: Use get_preset_mode() instead. Will be removed in 2026.5.0 - __attribute__((deprecated("Use get_preset_mode() instead of .preset_mode. Will be removed in 2026.5.0"))); + // Deprecated: Use get_preset_mode() for reading and set_preset_mode_() for writing. Will be removed in 2026.5.0 + __attribute__((deprecated("Use get_preset_mode() for reading and set_preset_mode_() for writing instead of " + ".preset_mode. Will be removed in 2026.5.0"))); std::string preset_mode{}; FanCall turn_on(); From 76952026b7d0d9ee9ac9be5ff66edd57f2cd1f45 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 31 Oct 2025 11:18:14 -0500 Subject: [PATCH 338/394] preen --- esphome/components/copy/fan/copy_fan.cpp | 12 ++---------- esphome/components/fan/fan.cpp | 8 ++++++++ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/esphome/components/copy/fan/copy_fan.cpp b/esphome/components/copy/fan/copy_fan.cpp index c1e873e083..d35ece950b 100644 --- a/esphome/components/copy/fan/copy_fan.cpp +++ b/esphome/components/copy/fan/copy_fan.cpp @@ -12,11 +12,7 @@ void CopyFan::setup() { this->oscillating = source_->oscillating; this->speed = source_->speed; this->direction = source_->direction; - const char *preset = source_->get_preset_mode(); - if (preset != nullptr) - this->set_preset_mode_(preset); - else - this->clear_preset_mode_(); + this->set_preset_mode_(source_->get_preset_mode()); this->publish_state(); }); @@ -24,11 +20,7 @@ void CopyFan::setup() { this->oscillating = source_->oscillating; this->speed = source_->speed; this->direction = source_->direction; - const char *preset = source_->get_preset_mode(); - if (preset != nullptr) - this->set_preset_mode_(preset); - else - this->clear_preset_mode_(); + this->set_preset_mode_(source_->get_preset_mode()); this->publish_state(); } diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index eb2e8743f3..c4abab0b4a 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -138,6 +138,14 @@ FanCall Fan::make_call() { return FanCall(*this); } const char *Fan::find_preset_mode_(const char *preset_mode) { return this->get_traits().find_preset_mode(preset_mode); } bool Fan::set_preset_mode_(const char *preset_mode) { + if (preset_mode == nullptr) { + // Treat nullptr as clearing the preset mode + if (this->preset_mode_ == nullptr) { + return false; // No change + } + this->clear_preset_mode_(); + return true; + } const char *validated = this->find_preset_mode_(preset_mode); if (validated == nullptr || this->preset_mode_ == validated) { return false; // Preset mode not supported or no change From 9dcfbed8af77761f682355f96a325bb1649ca5fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 31 Oct 2025 11:37:22 -0500 Subject: [PATCH 339/394] wip --- esphome/components/api/api_connection.cpp | 4 ++-- esphome/components/fan/fan.cpp | 14 +------------- esphome/components/fan/fan.h | 8 +++----- 3 files changed, 6 insertions(+), 20 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 33d5072d9c..90e37c8c59 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -410,8 +410,8 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co } if (traits.supports_direction()) msg.direction = static_cast(fan->direction); - if (traits.supports_preset_modes()) - msg.set_preset_mode(StringRef(fan->preset_mode)); + if (traits.supports_preset_modes() && fan->has_preset_mode()) + msg.set_preset_mode(StringRef(fan->get_preset_mode())); return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index c4abab0b4a..cfc09f4d53 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -151,24 +151,12 @@ bool Fan::set_preset_mode_(const char *preset_mode) { return false; // Preset mode not supported or no change } this->preset_mode_ = validated; - // Keep deprecated member in sync during deprecation period -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - this->preset_mode = validated; -#pragma GCC diagnostic pop return true; } bool Fan::set_preset_mode_(const std::string &preset_mode) { return this->set_preset_mode_(preset_mode.c_str()); } -void Fan::clear_preset_mode_() { - this->preset_mode_ = nullptr; - // Keep deprecated member in sync during deprecation period -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - this->preset_mode.clear(); -#pragma GCC diagnostic pop -} +void Fan::clear_preset_mode_() { this->preset_mode_ = nullptr; } void Fan::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } void Fan::publish_state() { diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index 16fe42f9f4..33e546b2bb 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -111,11 +111,6 @@ class Fan : public EntityBase { int speed{0}; /// The current direction of the fan FanDirection direction{FanDirection::FORWARD}; - // The current preset mode of the fan - // Deprecated: Use get_preset_mode() for reading and set_preset_mode_() for writing. Will be removed in 2026.5.0 - __attribute__((deprecated("Use get_preset_mode() for reading and set_preset_mode_() for writing instead of " - ".preset_mode. Will be removed in 2026.5.0"))); - std::string preset_mode{}; FanCall turn_on(); FanCall turn_off(); @@ -135,6 +130,9 @@ class Fan : public EntityBase { /// Get the current preset mode (returns pointer to string stored in traits, or nullptr if not set) const char *get_preset_mode() const { return this->preset_mode_; } + /// Check if a preset mode is currently active + bool has_preset_mode() const { return this->preset_mode_ != nullptr; } + protected: friend FanCall; friend struct FanRestoreState; From e6421ac50c2960a3cdb8b633da1986f49ccece9a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 31 Oct 2025 11:42:32 -0500 Subject: [PATCH 340/394] remove bugfix --- esphome/components/hbridge/fan/hbridge_fan.cpp | 5 +---- esphome/components/speed/fan/speed_fan.cpp | 5 +---- esphome/components/template/fan/template_fan.cpp | 5 +---- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 01680ae651..18591fb1ff 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -51,11 +51,8 @@ void HBridgeFan::dump_config() { void HBridgeFan::control(const fan::FanCall &call) { if (call.get_state().has_value()) this->state = *call.get_state(); - if (call.get_speed().has_value()) { + if (call.get_speed().has_value()) this->speed = *call.get_speed(); - // Speed manually set, clear preset mode - this->clear_preset_mode_(); - } if (call.get_oscillating().has_value()) this->oscillating = *call.get_oscillating(); if (call.get_direction().has_value()) diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 43b149e382..c1ccb0a0bb 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -23,11 +23,8 @@ void SpeedFan::dump_config() { LOG_FAN("", "Speed Fan", this); } void SpeedFan::control(const fan::FanCall &call) { if (call.get_state().has_value()) this->state = *call.get_state(); - if (call.get_speed().has_value()) { + if (call.get_speed().has_value()) this->speed = *call.get_speed(); - // Speed manually set, clear preset mode - this->clear_preset_mode_(); - } if (call.get_oscillating().has_value()) this->oscillating = *call.get_oscillating(); if (call.get_direction().has_value()) diff --git a/esphome/components/template/fan/template_fan.cpp b/esphome/components/template/fan/template_fan.cpp index 4ec7e121cf..7793fc0b7c 100644 --- a/esphome/components/template/fan/template_fan.cpp +++ b/esphome/components/template/fan/template_fan.cpp @@ -23,11 +23,8 @@ void TemplateFan::dump_config() { LOG_FAN("", "Template Fan", this); } void TemplateFan::control(const fan::FanCall &call) { if (call.get_state().has_value()) this->state = *call.get_state(); - if (call.get_speed().has_value() && (this->speed_count_ > 0)) { + if (call.get_speed().has_value() && (this->speed_count_ > 0)) this->speed = *call.get_speed(); - // Speed manually set, clear preset mode - this->clear_preset_mode_(); - } if (call.get_oscillating().has_value() && this->has_oscillating_) this->oscillating = *call.get_oscillating(); if (call.get_direction().has_value() && this->has_direction_) From d5938df53194267735ab0574a27eb5266204f346 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 31 Oct 2025 11:45:12 -0500 Subject: [PATCH 341/394] remove bugfix --- esphome/components/hbridge/fan/hbridge_fan.cpp | 3 +-- esphome/components/speed/fan/speed_fan.cpp | 3 +-- esphome/components/template/fan/template_fan.cpp | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 18591fb1ff..488208b725 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -57,8 +57,7 @@ void HBridgeFan::control(const fan::FanCall &call) { this->oscillating = *call.get_oscillating(); if (call.get_direction().has_value()) this->direction = *call.get_direction(); - if (call.has_preset_mode()) - this->set_preset_mode_(call.get_preset_mode()); + this->set_preset_mode_(call.get_preset_mode()); this->write_state_(); this->publish_state(); diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index c1ccb0a0bb..801593c2ac 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -29,8 +29,7 @@ void SpeedFan::control(const fan::FanCall &call) { this->oscillating = *call.get_oscillating(); if (call.get_direction().has_value()) this->direction = *call.get_direction(); - if (call.has_preset_mode()) - this->set_preset_mode_(call.get_preset_mode()); + this->set_preset_mode_(call.get_preset_mode()); this->write_state_(); this->publish_state(); diff --git a/esphome/components/template/fan/template_fan.cpp b/esphome/components/template/fan/template_fan.cpp index 7793fc0b7c..eba4c673b5 100644 --- a/esphome/components/template/fan/template_fan.cpp +++ b/esphome/components/template/fan/template_fan.cpp @@ -29,8 +29,7 @@ void TemplateFan::control(const fan::FanCall &call) { this->oscillating = *call.get_oscillating(); if (call.get_direction().has_value() && this->has_direction_) this->direction = *call.get_direction(); - if (call.has_preset_mode()) - this->set_preset_mode_(call.get_preset_mode()); + this->set_preset_mode_(call.get_preset_mode()); this->publish_state(); } From cbaa15635f80f961f40c187a9e300b6696cc815e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 31 Oct 2025 11:49:35 -0500 Subject: [PATCH 342/394] remove bugfix --- esphome/components/fan/automation.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index 90661c307c..048ba04646 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -212,18 +212,18 @@ class FanPresetSetTrigger : public Trigger { public: FanPresetSetTrigger(Fan *state) { state->add_on_state_callback([this, state]() { - auto preset_mode = state->preset_mode; + auto preset_mode = state->get_preset_mode(); auto should_trigger = preset_mode != this->last_preset_mode_; this->last_preset_mode_ = preset_mode; - if (should_trigger) { + if (should_trigger && preset_mode != nullptr) { this->trigger(preset_mode); } }); - this->last_preset_mode_ = state->preset_mode; + this->last_preset_mode_ = state->get_preset_mode(); } protected: - std::string last_preset_mode_; + const char *last_preset_mode_{nullptr}; }; } // namespace fan From 5c184777c673db996a351f84492b4ba5bd00fa5e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 31 Oct 2025 12:05:48 -0500 Subject: [PATCH 343/394] remove bugfix --- esphome/components/fan/automation.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index 048ba04646..48de8d66fb 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -212,7 +212,7 @@ class FanPresetSetTrigger : public Trigger { public: FanPresetSetTrigger(Fan *state) { state->add_on_state_callback([this, state]() { - auto preset_mode = state->get_preset_mode(); + const auto *preset_mode = state->get_preset_mode(); auto should_trigger = preset_mode != this->last_preset_mode_; this->last_preset_mode_ = preset_mode; if (should_trigger && preset_mode != nullptr) { From 6d0527ff2ad406603067a939d0b3f703db33fc0c Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Fri, 31 Oct 2025 20:04:55 +0100 Subject: [PATCH 344/394] [substitutions] fix jinja parsing strings that look like sets as sets (#11611) --- esphome/components/substitutions/jinja.py | 10 ++++++++-- .../fixtures/substitutions/00-simple_var.approved.yaml | 1 + .../fixtures/substitutions/00-simple_var.input.yaml | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/esphome/components/substitutions/jinja.py b/esphome/components/substitutions/jinja.py index cb3c6dfac5..fb9f843da2 100644 --- a/esphome/components/substitutions/jinja.py +++ b/esphome/components/substitutions/jinja.py @@ -138,6 +138,7 @@ def _concat_nodes_override(values: Iterator[Any]) -> Any: values = chain(head, values) raw = "".join([str(v) for v in values]) + result = None try: # Attempt to parse the concatenated string into a Python literal. # This allows expressions like "1 + 2" to be evaluated to the integer 3. @@ -145,11 +146,16 @@ def _concat_nodes_override(values: Iterator[Any]) -> Any: # fall back to returning the raw string. This is consistent with # Home Assistant's behavior when evaluating templates result = literal_eval(raw) + except (ValueError, SyntaxError, MemoryError, TypeError): + pass + else: + if isinstance(result, set): + # Sets are not supported, return raw string + return raw + if not isinstance(result, str): return result - except (ValueError, SyntaxError, MemoryError, TypeError): - pass return raw diff --git a/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml b/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml index 795a788f62..6f3bae1ac4 100644 --- a/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml @@ -33,3 +33,4 @@ test_list: {{{ "x", "79"}, { "y", "82"}}} - '{{{"AA"}}}' - '"HELLO"' + - '{ 79, 82 }' diff --git a/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml b/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml index 722e116d36..306119b753 100644 --- a/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml @@ -34,3 +34,4 @@ test_list: {{{ "x", "${ position.x }"}, { "y", "${ position.y }"}}} - ${ '{{{"AA"}}}' } - ${ '"HELLO"' } + - '{ ${position.x}, ${position.y} }' From 292abd1187f8de0b4941e26974e05581177abfe0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 19:46:50 +0000 Subject: [PATCH 345/394] Bump ruff from 0.14.2 to 0.14.3 (#11633) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f7e4a688e0..5356bffd96 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.2 + rev: v0.14.3 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index a11992b0fd..11367172b1 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.2 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.14.2 # also change in .pre-commit-config.yaml when updating +ruff==0.14.3 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.0 # also change in .pre-commit-config.yaml when updating pre-commit From 30f2a4395fd737bb723c1f82a56ec6f0ba4e41a0 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 1 Nov 2025 10:08:28 +1000 Subject: [PATCH 346/394] [image] Catch and report svg load errors (#11619) --- esphome/components/image/__init__.py | 31 +++++++++++++++++++++------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index f880b5f736..bf25a7cd92 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -671,18 +671,33 @@ async def write_image(config, all_frames=False): resize = config.get(CONF_RESIZE) if is_svg_file(path): # Local import so use of non-SVG files needn't require cairosvg installed + from pyexpat import ExpatError + from xml.etree.ElementTree import ParseError + from cairosvg import svg2png + from cairosvg.helpers import PointError if not resize: resize = (None, None) - with open(path, "rb") as file: - image = svg2png( - file_obj=file, - output_width=resize[0], - output_height=resize[1], - ) - image = Image.open(io.BytesIO(image)) - width, height = image.size + try: + with open(path, "rb") as file: + image = svg2png( + file_obj=file, + output_width=resize[0], + output_height=resize[1], + ) + image = Image.open(io.BytesIO(image)) + width, height = image.size + except ( + ValueError, + ParseError, + IndexError, + ExpatError, + AttributeError, + TypeError, + PointError, + ) as e: + raise core.EsphomeError(f"Could not load SVG image {path}: {e}") from e else: image = Image.open(path) width, height = image.size From 04222d2851194acd37628034eb3234ceb3451bb5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 31 Oct 2025 22:22:04 -0500 Subject: [PATCH 347/394] [web_server] Eliminate nested lambdas in DeferredUpdateEventSourceList --- esphome/components/web_server/web_server.cpp | 61 ++++++++++---------- esphome/components/web_server/web_server.h | 2 +- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 1d08ef5a35..d92a5382ba 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -220,50 +220,51 @@ void DeferredUpdateEventSourceList::add_new_client(WebServer *ws, AsyncWebServer DeferredUpdateEventSource *es = new DeferredUpdateEventSource(ws, "/events"); this->push_back(es); - es->onConnect([this, ws, es](AsyncEventSourceClient *client) { - ws->defer([this, ws, es]() { this->on_client_connect_(ws, es); }); - }); + es->onConnect([this, es](AsyncEventSourceClient *client) { this->on_client_connect_(es); }); - es->onDisconnect([this, ws, es](AsyncEventSourceClient *client) { - ws->defer([this, es]() { this->on_client_disconnect_((DeferredUpdateEventSource *) es); }); - }); + es->onDisconnect([this, es](AsyncEventSourceClient *client) { this->on_client_disconnect_(es); }); es->handleRequest(request); } -void DeferredUpdateEventSourceList::on_client_connect_(WebServer *ws, DeferredUpdateEventSource *source) { - // Configure reconnect timeout and send config - // this should always go through since the AsyncEventSourceClient event queue is empty on connect - std::string message = ws->get_config_json(); - source->try_send_nodefer(message.c_str(), "ping", millis(), 30000); +void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource *source) { + WebServer *ws = source->web_server_; + ws->defer([this, ws, source]() { + // Configure reconnect timeout and send config + // this should always go through since the AsyncEventSourceClient event queue is empty on connect + std::string message = ws->get_config_json(); + source->try_send_nodefer(message.c_str(), "ping", millis(), 30000); #ifdef USE_WEBSERVER_SORTING - for (auto &group : ws->sorting_groups_) { - json::JsonBuilder builder; - JsonObject root = builder.root(); - root["name"] = group.second.name; - root["sorting_weight"] = group.second.weight; - message = builder.serialize(); + for (auto &group : ws->sorting_groups_) { + json::JsonBuilder builder; + JsonObject root = builder.root(); + root["name"] = group.second.name; + root["sorting_weight"] = group.second.weight; + message = builder.serialize(); - // up to 31 groups should be able to be queued initially without defer - source->try_send_nodefer(message.c_str(), "sorting_group"); - } + // up to 31 groups should be able to be queued initially without defer + source->try_send_nodefer(message.c_str(), "sorting_group"); + } #endif - source->entities_iterator_.begin(ws->include_internal_); + source->entities_iterator_.begin(ws->include_internal_); - // just dump them all up-front and take advantage of the deferred queue - // on second thought that takes too long, but leaving the commented code here for debug purposes - // while(!source->entities_iterator_.completed()) { - // source->entities_iterator_.advance(); - //} + // just dump them all up-front and take advantage of the deferred queue + // on second thought that takes too long, but leaving the commented code here for debug purposes + // while(!source->entities_iterator_.completed()) { + // source->entities_iterator_.advance(); + //} + }); } void DeferredUpdateEventSourceList::on_client_disconnect_(DeferredUpdateEventSource *source) { - // This method was called via WebServer->defer() and is no longer executing in the - // context of the network callback. The object is now dead and can be safely deleted. - this->remove(source); - delete source; // NOLINT + source->web_server_->defer([this, source]() { + // This method was called via WebServer->defer() and is no longer executing in the + // context of the network callback. The object is now dead and can be safely deleted. + this->remove(source); + delete source; // NOLINT + }); } #endif diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 2e5d58d375..c54f5558a9 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -141,7 +141,7 @@ class DeferredUpdateEventSource : public AsyncEventSource { class DeferredUpdateEventSourceList : public std::list { protected: - void on_client_connect_(WebServer *ws, DeferredUpdateEventSource *source); + void on_client_connect_(DeferredUpdateEventSource *source); void on_client_disconnect_(DeferredUpdateEventSource *source); public: From ad0d6da2f39ef81cea5aefc8aea71c12ee81d324 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 31 Oct 2025 22:27:26 -0500 Subject: [PATCH 348/394] preen --- esphome/components/web_server/web_server.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index d92a5382ba..cc25d7a02f 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -229,7 +229,7 @@ void DeferredUpdateEventSourceList::add_new_client(WebServer *ws, AsyncWebServer void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource *source) { WebServer *ws = source->web_server_; - ws->defer([this, ws, source]() { + ws->defer([ws, source]() { // Configure reconnect timeout and send config // this should always go through since the AsyncEventSourceClient event queue is empty on connect std::string message = ws->get_config_json(); From c8f7bceb340669b46d23faa1ae407f5a6bede477 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 31 Oct 2025 22:56:02 -0500 Subject: [PATCH 349/394] [web_server] Remove redundant assignment in deq_push_back_with_dedup_ --- esphome/components/web_server/web_server.cpp | 3 +-- esphome/components/web_server_idf/web_server_idf.cpp | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 1d08ef5a35..61951e2600 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -111,8 +111,7 @@ void DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_ // Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size for (auto &event : this->deferred_queue_) { if (event == item) { - event = item; - return; + return; // Already in queue, no need to update since items are equal } } this->deferred_queue_.push_back(item); diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index c3ba7ddc2b..ac0b5bad83 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -494,8 +494,7 @@ void AsyncEventSourceResponse::deq_push_back_with_dedup_(void *source, message_g // Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size for (auto &event : this->deferred_queue_) { if (event == item) { - event = item; - return; + return; // Already in queue, no need to update since items are equal } } this->deferred_queue_.push_back(item); From d9d2d2f6b936369324ce271a509efa88e48aff1c Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 1 Nov 2025 14:17:23 +1000 Subject: [PATCH 350/394] [automations] Update error message (#11640) --- esphome/automation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/automation.py b/esphome/automation.py index cfe0af1b59..99be12451e 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -182,7 +182,7 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False): value = cv.Schema([extra_validators])(value) if single: if len(value) != 1: - raise cv.Invalid("Cannot have more than 1 automation for templates") + raise cv.Invalid("This trigger allows only a single automation") return value[0] return value From d2249ff8be092e6e11b72f401f10e5c8a4cc47ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 31 Oct 2025 23:27:00 -0500 Subject: [PATCH 351/394] [scheduler] Refactor call() for improved code organization --- esphome/core/scheduler.cpp | 105 +++++++++++-------------------------- esphome/core/scheduler.h | 58 ++++++++++++++++++++ 2 files changed, 88 insertions(+), 75 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 0d4715f621..11d59c2499 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -316,59 +316,37 @@ optional HOT Scheduler::next_schedule_in(uint32_t now) { return 0; return next_exec - now_64; } + +void Scheduler::full_cleanup_removed_items_() { + // We hold the lock for the entire cleanup operation because: + // 1. We're rebuilding the entire items_ list, so we need exclusive access throughout + // 2. Other threads must see either the old state or the new state, not intermediate states + // 3. The operation is already expensive (O(n)), so lock overhead is negligible + // 4. No operations inside can block or take other locks, so no deadlock risk + LockGuard guard{this->lock_}; + + std::vector> valid_items; + + // Move all non-removed items to valid_items, recycle removed ones + for (auto &item : this->items_) { + if (!is_item_removed_(item.get())) { + valid_items.push_back(std::move(item)); + } else { + // Recycle removed items + this->recycle_item_(std::move(item)); + } + } + + // Replace items_ with the filtered list + this->items_ = std::move(valid_items); + // Rebuild the heap structure since items are no longer in heap order + std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); + this->to_remove_ = 0; +} + void HOT Scheduler::call(uint32_t now) { #ifndef ESPHOME_THREAD_SINGLE - // Process defer queue first to guarantee FIFO execution order for deferred items. - // Previously, defer() used the heap which gave undefined order for equal timestamps, - // causing race conditions on multi-core systems (ESP32, BK7200). - // With the defer queue: - // - Deferred items (delay=0) go directly to defer_queue_ in set_timer_common_ - // - Items execute in exact order they were deferred (FIFO guarantee) - // - No deferred items exist in to_add_, so processing order doesn't affect correctness - // Single-core platforms don't use this queue and fall back to the heap-based approach. - // - // Note: Items cancelled via cancel_item_locked_() are marked with remove=true but still - // processed here. They are skipped during execution by should_skip_item_(). - // This is intentional - no memory leak occurs. - // - // We use an index (defer_queue_front_) to track the read position instead of calling - // erase() on every pop, which would be O(n). The queue is processed once per loop - - // any items added during processing are left for the next loop iteration. - - // Snapshot the queue end point - only process items that existed at loop start - // Items added during processing (by callbacks or other threads) run next loop - // No lock needed: single consumer (main loop), stale read just means we process less this iteration - size_t defer_queue_end = this->defer_queue_.size(); - - while (this->defer_queue_front_ < defer_queue_end) { - std::unique_ptr item; - { - LockGuard lock(this->lock_); - // SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_. - // This is intentional and safe because: - // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function - // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_ - // and has_cancelled_timeout_in_container_ 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_++; - } - - // Execute callback without holding lock to prevent deadlocks - // if the callback tries to call defer() again - if (!this->should_skip_item_(item.get())) { - now = this->execute_item_(item.get(), now); - } - // Recycle the defer item after execution - this->recycle_item_(std::move(item)); - } - - // If we've consumed all items up to the snapshot point, clean up the dead space - // Single consumer (main loop), so no lock needed for this check - if (this->defer_queue_front_ >= defer_queue_end) { - LockGuard lock(this->lock_); - this->cleanup_defer_queue_locked_(); - } + this->process_defer_queue_(now); #endif /* not ESPHOME_THREAD_SINGLE */ // Convert the fresh timestamp from main loop to 64-bit for scheduler operations @@ -429,30 +407,7 @@ void HOT Scheduler::call(uint32_t now) { // If we still have too many cancelled items, do a full cleanup // This only happens if cancelled items are stuck in the middle/bottom of the heap if (this->to_remove_ >= MAX_LOGICALLY_DELETED_ITEMS) { - // We hold the lock for the entire cleanup operation because: - // 1. We're rebuilding the entire items_ list, so we need exclusive access throughout - // 2. Other threads must see either the old state or the new state, not intermediate states - // 3. The operation is already expensive (O(n)), so lock overhead is negligible - // 4. No operations inside can block or take other locks, so no deadlock risk - LockGuard guard{this->lock_}; - - std::vector> valid_items; - - // Move all non-removed items to valid_items, recycle removed ones - for (auto &item : this->items_) { - if (!is_item_removed_(item.get())) { - valid_items.push_back(std::move(item)); - } else { - // Recycle removed items - this->recycle_item_(std::move(item)); - } - } - - // Replace items_ with the filtered list - this->items_ = std::move(valid_items); - // Rebuild the heap structure since items are no longer in heap order - std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); - this->to_remove_ = 0; + this->full_cleanup_removed_items_(); } while (!this->items_.empty()) { // Don't copy-by value yet diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index df0be0e4ce..f6ec07294d 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -263,7 +263,65 @@ class Scheduler { // Helper to recycle a SchedulerItem void recycle_item_(std::unique_ptr item); + // Helper to perform full cleanup when too many items are cancelled + void full_cleanup_removed_items_(); + #ifndef ESPHOME_THREAD_SINGLE + // Helper to process defer queue - inline for performance in hot path + inline void process_defer_queue_(uint32_t &now) { + // Process defer queue first to guarantee FIFO execution order for deferred items. + // Previously, defer() used the heap which gave undefined order for equal timestamps, + // causing race conditions on multi-core systems (ESP32, BK7200). + // With the defer queue: + // - Deferred items (delay=0) go directly to defer_queue_ in set_timer_common_ + // - Items execute in exact order they were deferred (FIFO guarantee) + // - No deferred items exist in to_add_, so processing order doesn't affect correctness + // Single-core platforms don't use this queue and fall back to the heap-based approach. + // + // Note: Items cancelled via cancel_item_locked_() are marked with remove=true but still + // processed here. They are skipped during execution by should_skip_item_(). + // This is intentional - no memory leak occurs. + // + // We use an index (defer_queue_front_) to track the read position instead of calling + // erase() on every pop, which would be O(n). The queue is processed once per loop - + // any items added during processing are left for the next loop iteration. + + // Snapshot the queue end point - only process items that existed at loop start + // Items added during processing (by callbacks or other threads) run next loop + // No lock needed: single consumer (main loop), stale read just means we process less this iteration + size_t defer_queue_end = this->defer_queue_.size(); + + while (this->defer_queue_front_ < defer_queue_end) { + std::unique_ptr item; + { + LockGuard lock(this->lock_); + // SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_. + // This is intentional and safe because: + // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function + // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_ + // and has_cancelled_timeout_in_container_ 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_++; + } + + // Execute callback without holding lock to prevent deadlocks + // if the callback tries to call defer() again + if (!this->should_skip_item_(item.get())) { + now = this->execute_item_(item.get(), now); + } + // Recycle the defer item after execution + this->recycle_item_(std::move(item)); + } + + // If we've consumed all items up to the snapshot point, clean up the dead space + // Single consumer (main loop), so no lock needed for this check + if (this->defer_queue_front_ >= defer_queue_end) { + LockGuard lock(this->lock_); + this->cleanup_defer_queue_locked_(); + } + } + // Helper to cleanup defer_queue_ after processing // IMPORTANT: Caller must hold the scheduler lock before calling this function. inline void cleanup_defer_queue_locked_() { From ab261f343686bcc593c187e4f7a2c29e70d1962a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Nov 2025 00:19:54 -0500 Subject: [PATCH 352/394] [web_server] Use zero-copy entity ID comparison in request handlers --- esphome/components/web_server/web_server.cpp | 52 +++++++++++--------- esphome/components/web_server/web_server.h | 11 ++++- esphome/core/entity_base.h | 5 ++ 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 1d08ef5a35..991a263acd 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -435,9 +435,10 @@ void WebServer::on_sensor_update(sensor::Sensor *obj, float state) { } void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (sensor::Sensor *obj : App.get_sensors()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + // Note: request->method() is always HTTP_GET here (canHandle ensures this) + if (match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->sensor_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -477,9 +478,10 @@ void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::s } void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (text_sensor::TextSensor *obj : App.get_text_sensors()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + // Note: request->method() is always HTTP_GET here (canHandle ensures this) + if (match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->text_sensor_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -516,7 +518,7 @@ void WebServer::on_switch_update(switch_::Switch *obj, bool state) { } void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (switch_::Switch *obj : App.get_switches()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -585,7 +587,7 @@ std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail #ifdef USE_BUTTON void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (button::Button *obj : App.get_buttons()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); @@ -627,9 +629,10 @@ void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) { } void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (binary_sensor::BinarySensor *obj : App.get_binary_sensors()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + // Note: request->method() is always HTTP_GET here (canHandle ensures this) + if (match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->binary_sensor_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -665,7 +668,7 @@ void WebServer::on_fan_update(fan::Fan *obj) { } void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (fan::Fan *obj : App.get_fans()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -739,7 +742,7 @@ void WebServer::on_light_update(light::LightState *obj) { } void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (light::LightState *obj : App.get_lights()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -812,7 +815,7 @@ void WebServer::on_cover_update(cover::Cover *obj) { } void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (cover::Cover *obj : App.get_covers()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -897,7 +900,7 @@ void WebServer::on_number_update(number::Number *obj, float state) { } void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_numbers()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -962,7 +965,7 @@ void WebServer::on_date_update(datetime::DateEntity *obj) { } void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_dates()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); @@ -1017,7 +1020,7 @@ void WebServer::on_time_update(datetime::TimeEntity *obj) { } void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_times()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); @@ -1071,7 +1074,7 @@ void WebServer::on_datetime_update(datetime::DateTimeEntity *obj) { } void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_datetimes()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); @@ -1126,7 +1129,7 @@ void WebServer::on_text_update(text::Text *obj, const std::string &state) { } void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_texts()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -1180,7 +1183,7 @@ void WebServer::on_select_update(select::Select *obj, const std::string &state, } void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_selects()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -1236,7 +1239,7 @@ void WebServer::on_climate_update(climate::Climate *obj) { } void WebServer::handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_climates()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -1377,7 +1380,7 @@ void WebServer::on_lock_update(lock::Lock *obj) { } void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (lock::Lock *obj : App.get_locks()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -1448,7 +1451,7 @@ void WebServer::on_valve_update(valve::Valve *obj) { } void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (valve::Valve *obj : App.get_valves()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -1529,7 +1532,7 @@ void WebServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlP } void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (alarm_control_panel::AlarmControlPanel *obj : App.get_alarm_control_panels()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -1608,10 +1611,11 @@ void WebServer::on_event(event::Event *obj, const std::string &event_type) { void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (event::Event *obj : App.get_events()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + // Note: request->method() is always HTTP_GET here (canHandle ensures this) + if (match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->event_json(obj, "", detail); request->send(200, "application/json", data.c_str()); @@ -1673,7 +1677,7 @@ void WebServer::on_update(update::UpdateEntity *obj) { } void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (update::UpdateEntity *obj : App.get_updates()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 2e5d58d375..8b5558b5b9 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -48,8 +48,15 @@ struct UrlMatch { return domain && domain_len == strlen(str) && memcmp(domain, str, domain_len) == 0; } - bool id_equals(const std::string &str) const { - return id && id_len == str.length() && memcmp(id, str.c_str(), id_len) == 0; + bool id_equals_entity(EntityBase *entity) const { + // Zero-copy comparison using StringRef + StringRef static_ref = entity->get_object_id_ref_for_api_(); + if (!static_ref.empty()) { + return id && id_len == static_ref.size() && memcmp(id, static_ref.c_str(), id_len) == 0; + } + // Fallback to allocation (rare) + const auto &obj_id = entity->get_object_id(); + return id && id_len == obj_id.length() && memcmp(id, obj_id.c_str(), id_len) == 0; } bool method_equals(const char *str) const { diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 4a6460e708..80cd6b8e77 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -17,6 +17,10 @@ namespace api { class APIConnection; } // namespace api +namespace web_server { +struct UrlMatch; +} // namespace web_server + enum EntityCategory : uint8_t { ENTITY_CATEGORY_NONE = 0, ENTITY_CATEGORY_CONFIG = 1, @@ -116,6 +120,7 @@ class EntityBase { protected: friend class api::APIConnection; + friend struct web_server::UrlMatch; // Get object_id as StringRef when it's static (for API usage) // Returns empty StringRef if object_id is dynamic (needs allocation) From 5a5894eaa3049a174f44c9af185bcd3c080cae5d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Nov 2025 01:05:26 -0500 Subject: [PATCH 353/394] [ruff] Remove deprecated UP038 rule from ignore list (#11646) --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 49598d434d..d6aa584237 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -133,7 +133,6 @@ ignore = [ "PLW1641", # Object does not implement `__hash__` method "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target - "UP038", # https://github.com/astral-sh/ruff/issues/7871 https://github.com/astral-sh/ruff/pull/16681 ] [tool.ruff.lint.isort] From 8df5a3a6308a1b01c0a4137a7b68f0769d55e2a5 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 1 Nov 2025 16:27:28 +1000 Subject: [PATCH 354/394] [lvgl] Trigger improvements and additions (#11628) --- esphome/components/lvgl/__init__.py | 52 +++++++++++++---------- esphome/components/lvgl/automation.py | 3 +- esphome/components/lvgl/defines.py | 2 + esphome/components/lvgl/lvgl_esphome.cpp | 33 ++++++++++---- esphome/components/lvgl/lvgl_esphome.h | 27 +++++++----- esphome/components/lvgl/types.py | 6 ++- tests/components/lvgl/test.esp32-idf.yaml | 8 ++++ 7 files changed, 86 insertions(+), 45 deletions(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 5af61300da..aa6935c5fc 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -58,7 +58,7 @@ from .types import ( FontEngine, IdleTrigger, ObjUpdateAction, - PauseTrigger, + PlainTrigger, lv_font_t, lv_group_t, lv_style_t, @@ -151,6 +151,13 @@ for w_type in WIDGET_TYPES.values(): create_modify_schema(w_type), )(update_to_code) +SIMPLE_TRIGGERS = ( + df.CONF_ON_PAUSE, + df.CONF_ON_RESUME, + df.CONF_ON_DRAW_START, + df.CONF_ON_DRAW_END, +) + def as_macro(macro, value): if value is None: @@ -244,9 +251,9 @@ def final_validation(configs): for w in refreshed_widgets: path = global_config.get_path_for_id(w) widget_conf = global_config.get_config_for_path(path[:-1]) - if not any(isinstance(v, Lambda) for v in widget_conf.values()): + if not any(isinstance(v, (Lambda, dict)) for v in widget_conf.values()): raise cv.Invalid( - f"Widget '{w}' does not have any templated properties to refresh", + f"Widget '{w}' does not have any dynamic properties to refresh", ) @@ -366,16 +373,16 @@ async def to_code(configs): conf[CONF_TRIGGER_ID], lv_component, templ ) await build_automation(idle_trigger, [], conf) - for conf in config.get(df.CONF_ON_PAUSE, ()): - pause_trigger = cg.new_Pvariable( - conf[CONF_TRIGGER_ID], lv_component, True - ) - await build_automation(pause_trigger, [], conf) - for conf in config.get(df.CONF_ON_RESUME, ()): - resume_trigger = cg.new_Pvariable( - conf[CONF_TRIGGER_ID], lv_component, False - ) - await build_automation(resume_trigger, [], conf) + for trigger_name in SIMPLE_TRIGGERS: + if conf := config.get(trigger_name): + trigger_var = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + await build_automation(trigger_var, [], conf) + cg.add( + getattr( + lv_component, + f"set_{trigger_name.removeprefix('on_')}_trigger", + )(trigger_var) + ) await add_on_boot_triggers(config.get(CONF_ON_BOOT, ())) # This must be done after all widgets are created @@ -443,16 +450,15 @@ LVGL_SCHEMA = cv.All( ), } ), - cv.Optional(df.CONF_ON_PAUSE): validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger), - } - ), - cv.Optional(df.CONF_ON_RESUME): validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger), - } - ), + **{ + cv.Optional(x): validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PlainTrigger), + }, + single=True, + ) + for x in SIMPLE_TRIGGERS + }, cv.Exclusive(df.CONF_WIDGETS, CONF_PAGES): cv.ensure_list( WIDGET_SCHEMA ), diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index fc70b0f682..593c8c67bb 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -400,7 +400,8 @@ async def obj_refresh_to_code(config, action_id, template_arg, args): # must pass all widget-specific options here, even if not templated, but only do so if at least one is # templated. First filter out common style properties. config = {k: v for k, v in widget.config.items() if k not in ALL_STYLES} - if any(isinstance(v, Lambda) for v in config.values()): + # Check if v is a Lambda or a dict, implying it is dynamic + if any(isinstance(v, (Lambda, dict)) for v in config.values()): await widget.type.to_code(widget, config) if ( widget.type.w_type.value_property is not None diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 7fbb6de071..3241ba9c3f 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -483,6 +483,8 @@ CONF_MSGBOXES = "msgboxes" CONF_OBJ = "obj" CONF_ONE_CHECKED = "one_checked" CONF_ONE_LINE = "one_line" +CONF_ON_DRAW_START = "on_draw_start" +CONF_ON_DRAW_END = "on_draw_end" CONF_ON_PAUSE = "on_pause" CONF_ON_RESUME = "on_resume" CONF_ON_SELECT = "on_select" diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 7a32691b53..947342089c 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -82,6 +82,18 @@ static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) { area->y2 = (area->y2 + draw_rounding) / draw_rounding * draw_rounding - 1; } +void LvglComponent::monitor_cb(lv_disp_drv_t *disp_drv, uint32_t time, uint32_t px) { + ESP_LOGVV(TAG, "Draw end: %" PRIu32 " pixels in %" PRIu32 " ms", px, time); + auto *comp = static_cast(disp_drv->user_data); + comp->draw_end_(); +} + +void LvglComponent::render_start_cb(lv_disp_drv_t *disp_drv) { + ESP_LOGVV(TAG, "Draw start"); + auto *comp = static_cast(disp_drv->user_data); + comp->draw_start_(); +} + lv_event_code_t lv_api_event; // NOLINT lv_event_code_t lv_update_event; // NOLINT void LvglComponent::dump_config() { @@ -101,7 +113,10 @@ void LvglComponent::set_paused(bool paused, bool show_snow) { lv_disp_trig_activity(this->disp_); // resets the inactivity time lv_obj_invalidate(lv_scr_act()); } - this->pause_callbacks_.call(paused); + if (paused && this->pause_callback_ != nullptr) + this->pause_callback_->trigger(); + if (!paused && this->resume_callback_ != nullptr) + this->resume_callback_->trigger(); } void LvglComponent::esphome_lvgl_init() { @@ -225,13 +240,6 @@ IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableValue timeo }); } -PauseTrigger::PauseTrigger(LvglComponent *parent, TemplatableValue paused) : paused_(std::move(paused)) { - parent->add_on_pause_callback([this](bool pausing) { - if (this->paused_.value() == pausing) - this->trigger(); - }); -} - #ifdef USE_LVGL_TOUCHSCREEN LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time, LvglComponent *parent) { this->set_parent(parent); @@ -474,6 +482,12 @@ void LvglComponent::setup() { return; } } + if (this->draw_start_callback_ != nullptr) { + this->disp_drv_.render_start_cb = render_start_cb; + } + if (this->draw_end_callback_ != nullptr) { + this->disp_drv_.monitor_cb = monitor_cb; + } #if LV_USE_LOG lv_log_register_print_cb([](const char *buf) { auto next = strchr(buf, ')'); @@ -502,8 +516,9 @@ void LvglComponent::loop() { if (this->paused_) { if (this->show_snow_) this->write_random_(); + } else { + lv_timer_handler_run_in_period(5); } - lv_timer_handler_run_in_period(5); } #ifdef USE_LVGL_ANIMIMG diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index d3dc8fac5a..ea58fdb85b 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -171,7 +171,9 @@ class LvglComponent : public PollingComponent { void add_on_idle_callback(std::function &&callback) { this->idle_callbacks_.add(std::move(callback)); } - void add_on_pause_callback(std::function &&callback) { this->pause_callbacks_.add(std::move(callback)); } + + static void monitor_cb(lv_disp_drv_t *disp_drv, uint32_t time, uint32_t px); + static void render_start_cb(lv_disp_drv_t *disp_drv); void dump_config() override; bool is_idle(uint32_t idle_ms) { return lv_disp_get_inactive_time(this->disp_) > idle_ms; } lv_disp_t *get_disp() { return this->disp_; } @@ -213,12 +215,20 @@ class LvglComponent : public PollingComponent { size_t draw_rounding{2}; display::DisplayRotation rotation{display::DISPLAY_ROTATION_0_DEGREES}; + void set_pause_trigger(Trigger<> *trigger) { this->pause_callback_ = trigger; } + void set_resume_trigger(Trigger<> *trigger) { this->resume_callback_ = trigger; } + void set_draw_start_trigger(Trigger<> *trigger) { this->draw_start_callback_ = trigger; } + void set_draw_end_trigger(Trigger<> *trigger) { this->draw_end_callback_ = trigger; } protected: + // these functions are never called unless the callbacks are non-null since the + // LVGL callbacks that call them are not set unless the start/end callbacks are non-null + void draw_start_() const { this->draw_start_callback_->trigger(); } + void draw_end_() const { this->draw_end_callback_->trigger(); } + void write_random_(); void draw_buffer_(const lv_area_t *area, lv_color_t *ptr); void flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p); - std::vector displays_{}; size_t buffer_frac_{1}; bool full_refresh_{}; @@ -235,7 +245,10 @@ class LvglComponent : public PollingComponent { std::map focus_marks_{}; CallbackManager idle_callbacks_{}; - CallbackManager pause_callbacks_{}; + Trigger<> *pause_callback_{}; + Trigger<> *resume_callback_{}; + Trigger<> *draw_start_callback_{}; + Trigger<> *draw_end_callback_{}; lv_color_t *rotate_buf_{}; }; @@ -248,14 +261,6 @@ class IdleTrigger : public Trigger<> { bool is_idle_{}; }; -class PauseTrigger : public Trigger<> { - public: - explicit PauseTrigger(LvglComponent *parent, TemplatableValue paused); - - protected: - TemplatableValue paused_; -}; - template class LvglAction : public Action, public Parented { public: explicit LvglAction(std::function &&lamb) : action_(std::move(lamb)) {} diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index c19c89401a..9955b530aa 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -3,6 +3,7 @@ import sys from esphome import automation, codegen as cg from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_TEXT, CONF_VALUE from esphome.cpp_generator import MockObj, MockObjClass +from esphome.cpp_types import esphome_ns from .defines import lvgl_ns from .lvcode import lv_expr @@ -42,8 +43,11 @@ 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) +) IdleTrigger = lvgl_ns.class_("IdleTrigger", automation.Trigger.template()) -PauseTrigger = lvgl_ns.class_("PauseTrigger", automation.Trigger.template()) ObjUpdateAction = lvgl_ns.class_("ObjUpdateAction", automation.Action) LvglCondition = lvgl_ns.class_("LvglCondition", automation.Condition) LvglAction = lvgl_ns.class_("LvglAction", automation.Action) diff --git a/tests/components/lvgl/test.esp32-idf.yaml b/tests/components/lvgl/test.esp32-idf.yaml index 6170b0f4fb..2450d28eb8 100644 --- a/tests/components/lvgl/test.esp32-idf.yaml +++ b/tests/components/lvgl/test.esp32-idf.yaml @@ -68,5 +68,13 @@ lvgl: enter_button: pushbutton group: general initial_focus: lv_roller + on_draw_start: + - logger.log: draw started + on_draw_end: + - logger.log: draw ended + - lvgl.pause: + - component.update: tft_display + - delay: 60s + - lvgl.resume: <<: !include common.yaml From 4d1d37a9112a6b54467722531438349d672d0f5a Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 1 Nov 2025 16:37:07 +1000 Subject: [PATCH 355/394] [lvgl] Fix event for binary sensor (#11636) --- esphome/components/lvgl/binary_sensor/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/lvgl/binary_sensor/__init__.py b/esphome/components/lvgl/binary_sensor/__init__.py index ffbdc977b2..f9df7d23fa 100644 --- a/esphome/components/lvgl/binary_sensor/__init__.py +++ b/esphome/components/lvgl/binary_sensor/__init__.py @@ -31,7 +31,7 @@ async def to_code(config): lvgl_static.add_event_cb( widget.obj, await pressed_ctx.get_lambda(), - LV_EVENT.PRESSING, + LV_EVENT.PRESSED, LV_EVENT.RELEASED, ) ) From 0b4d445794f1d804b072d73d9ae0952139a48524 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 1 Nov 2025 16:45:42 +1000 Subject: [PATCH 356/394] [sdl] Fix keymappings (#11635) --- esphome/components/sdl/binary_sensor.py | 485 ++++++++++++------------ tests/components/sdl/common.yaml | 6 +- 2 files changed, 253 insertions(+), 238 deletions(-) diff --git a/esphome/components/sdl/binary_sensor.py b/esphome/components/sdl/binary_sensor.py index 3ea6c2d218..e19a488800 100644 --- a/esphome/components/sdl/binary_sensor.py +++ b/esphome/components/sdl/binary_sensor.py @@ -12,241 +12,256 @@ CODEOWNERS = ["@bdm310"] STATE_ARG = "state" -SDL_KEYMAP = { - "SDLK_UNKNOWN": 0, - "SDLK_FIRST": 0, - "SDLK_BACKSPACE": 8, - "SDLK_TAB": 9, - "SDLK_CLEAR": 12, - "SDLK_RETURN": 13, - "SDLK_PAUSE": 19, - "SDLK_ESCAPE": 27, - "SDLK_SPACE": 32, - "SDLK_EXCLAIM": 33, - "SDLK_QUOTEDBL": 34, - "SDLK_HASH": 35, - "SDLK_DOLLAR": 36, - "SDLK_AMPERSAND": 38, - "SDLK_QUOTE": 39, - "SDLK_LEFTPAREN": 40, - "SDLK_RIGHTPAREN": 41, - "SDLK_ASTERISK": 42, - "SDLK_PLUS": 43, - "SDLK_COMMA": 44, - "SDLK_MINUS": 45, - "SDLK_PERIOD": 46, - "SDLK_SLASH": 47, - "SDLK_0": 48, - "SDLK_1": 49, - "SDLK_2": 50, - "SDLK_3": 51, - "SDLK_4": 52, - "SDLK_5": 53, - "SDLK_6": 54, - "SDLK_7": 55, - "SDLK_8": 56, - "SDLK_9": 57, - "SDLK_COLON": 58, - "SDLK_SEMICOLON": 59, - "SDLK_LESS": 60, - "SDLK_EQUALS": 61, - "SDLK_GREATER": 62, - "SDLK_QUESTION": 63, - "SDLK_AT": 64, - "SDLK_LEFTBRACKET": 91, - "SDLK_BACKSLASH": 92, - "SDLK_RIGHTBRACKET": 93, - "SDLK_CARET": 94, - "SDLK_UNDERSCORE": 95, - "SDLK_BACKQUOTE": 96, - "SDLK_a": 97, - "SDLK_b": 98, - "SDLK_c": 99, - "SDLK_d": 100, - "SDLK_e": 101, - "SDLK_f": 102, - "SDLK_g": 103, - "SDLK_h": 104, - "SDLK_i": 105, - "SDLK_j": 106, - "SDLK_k": 107, - "SDLK_l": 108, - "SDLK_m": 109, - "SDLK_n": 110, - "SDLK_o": 111, - "SDLK_p": 112, - "SDLK_q": 113, - "SDLK_r": 114, - "SDLK_s": 115, - "SDLK_t": 116, - "SDLK_u": 117, - "SDLK_v": 118, - "SDLK_w": 119, - "SDLK_x": 120, - "SDLK_y": 121, - "SDLK_z": 122, - "SDLK_DELETE": 127, - "SDLK_WORLD_0": 160, - "SDLK_WORLD_1": 161, - "SDLK_WORLD_2": 162, - "SDLK_WORLD_3": 163, - "SDLK_WORLD_4": 164, - "SDLK_WORLD_5": 165, - "SDLK_WORLD_6": 166, - "SDLK_WORLD_7": 167, - "SDLK_WORLD_8": 168, - "SDLK_WORLD_9": 169, - "SDLK_WORLD_10": 170, - "SDLK_WORLD_11": 171, - "SDLK_WORLD_12": 172, - "SDLK_WORLD_13": 173, - "SDLK_WORLD_14": 174, - "SDLK_WORLD_15": 175, - "SDLK_WORLD_16": 176, - "SDLK_WORLD_17": 177, - "SDLK_WORLD_18": 178, - "SDLK_WORLD_19": 179, - "SDLK_WORLD_20": 180, - "SDLK_WORLD_21": 181, - "SDLK_WORLD_22": 182, - "SDLK_WORLD_23": 183, - "SDLK_WORLD_24": 184, - "SDLK_WORLD_25": 185, - "SDLK_WORLD_26": 186, - "SDLK_WORLD_27": 187, - "SDLK_WORLD_28": 188, - "SDLK_WORLD_29": 189, - "SDLK_WORLD_30": 190, - "SDLK_WORLD_31": 191, - "SDLK_WORLD_32": 192, - "SDLK_WORLD_33": 193, - "SDLK_WORLD_34": 194, - "SDLK_WORLD_35": 195, - "SDLK_WORLD_36": 196, - "SDLK_WORLD_37": 197, - "SDLK_WORLD_38": 198, - "SDLK_WORLD_39": 199, - "SDLK_WORLD_40": 200, - "SDLK_WORLD_41": 201, - "SDLK_WORLD_42": 202, - "SDLK_WORLD_43": 203, - "SDLK_WORLD_44": 204, - "SDLK_WORLD_45": 205, - "SDLK_WORLD_46": 206, - "SDLK_WORLD_47": 207, - "SDLK_WORLD_48": 208, - "SDLK_WORLD_49": 209, - "SDLK_WORLD_50": 210, - "SDLK_WORLD_51": 211, - "SDLK_WORLD_52": 212, - "SDLK_WORLD_53": 213, - "SDLK_WORLD_54": 214, - "SDLK_WORLD_55": 215, - "SDLK_WORLD_56": 216, - "SDLK_WORLD_57": 217, - "SDLK_WORLD_58": 218, - "SDLK_WORLD_59": 219, - "SDLK_WORLD_60": 220, - "SDLK_WORLD_61": 221, - "SDLK_WORLD_62": 222, - "SDLK_WORLD_63": 223, - "SDLK_WORLD_64": 224, - "SDLK_WORLD_65": 225, - "SDLK_WORLD_66": 226, - "SDLK_WORLD_67": 227, - "SDLK_WORLD_68": 228, - "SDLK_WORLD_69": 229, - "SDLK_WORLD_70": 230, - "SDLK_WORLD_71": 231, - "SDLK_WORLD_72": 232, - "SDLK_WORLD_73": 233, - "SDLK_WORLD_74": 234, - "SDLK_WORLD_75": 235, - "SDLK_WORLD_76": 236, - "SDLK_WORLD_77": 237, - "SDLK_WORLD_78": 238, - "SDLK_WORLD_79": 239, - "SDLK_WORLD_80": 240, - "SDLK_WORLD_81": 241, - "SDLK_WORLD_82": 242, - "SDLK_WORLD_83": 243, - "SDLK_WORLD_84": 244, - "SDLK_WORLD_85": 245, - "SDLK_WORLD_86": 246, - "SDLK_WORLD_87": 247, - "SDLK_WORLD_88": 248, - "SDLK_WORLD_89": 249, - "SDLK_WORLD_90": 250, - "SDLK_WORLD_91": 251, - "SDLK_WORLD_92": 252, - "SDLK_WORLD_93": 253, - "SDLK_WORLD_94": 254, - "SDLK_WORLD_95": 255, - "SDLK_KP0": 256, - "SDLK_KP1": 257, - "SDLK_KP2": 258, - "SDLK_KP3": 259, - "SDLK_KP4": 260, - "SDLK_KP5": 261, - "SDLK_KP6": 262, - "SDLK_KP7": 263, - "SDLK_KP8": 264, - "SDLK_KP9": 265, - "SDLK_KP_PERIOD": 266, - "SDLK_KP_DIVIDE": 267, - "SDLK_KP_MULTIPLY": 268, - "SDLK_KP_MINUS": 269, - "SDLK_KP_PLUS": 270, - "SDLK_KP_ENTER": 271, - "SDLK_KP_EQUALS": 272, - "SDLK_UP": 273, - "SDLK_DOWN": 274, - "SDLK_RIGHT": 275, - "SDLK_LEFT": 276, - "SDLK_INSERT": 277, - "SDLK_HOME": 278, - "SDLK_END": 279, - "SDLK_PAGEUP": 280, - "SDLK_PAGEDOWN": 281, - "SDLK_F1": 282, - "SDLK_F2": 283, - "SDLK_F3": 284, - "SDLK_F4": 285, - "SDLK_F5": 286, - "SDLK_F6": 287, - "SDLK_F7": 288, - "SDLK_F8": 289, - "SDLK_F9": 290, - "SDLK_F10": 291, - "SDLK_F11": 292, - "SDLK_F12": 293, - "SDLK_F13": 294, - "SDLK_F14": 295, - "SDLK_F15": 296, - "SDLK_NUMLOCK": 300, - "SDLK_CAPSLOCK": 301, - "SDLK_SCROLLOCK": 302, - "SDLK_RSHIFT": 303, - "SDLK_LSHIFT": 304, - "SDLK_RCTRL": 305, - "SDLK_LCTRL": 306, - "SDLK_RALT": 307, - "SDLK_LALT": 308, - "SDLK_RMETA": 309, - "SDLK_LMETA": 310, - "SDLK_LSUPER": 311, - "SDLK_RSUPER": 312, - "SDLK_MODE": 313, - "SDLK_COMPOSE": 314, - "SDLK_HELP": 315, - "SDLK_PRINT": 316, - "SDLK_SYSREQ": 317, - "SDLK_BREAK": 318, - "SDLK_MENU": 319, - "SDLK_POWER": 320, - "SDLK_EURO": 321, - "SDLK_UNDO": 322, -} +SDL_KeyCode = cg.global_ns.enum("SDL_KeyCode") + +SDL_KEYS = ( + "SDLK_UNKNOWN", + "SDLK_RETURN", + "SDLK_ESCAPE", + "SDLK_BACKSPACE", + "SDLK_TAB", + "SDLK_SPACE", + "SDLK_EXCLAIM", + "SDLK_QUOTEDBL", + "SDLK_HASH", + "SDLK_PERCENT", + "SDLK_DOLLAR", + "SDLK_AMPERSAND", + "SDLK_QUOTE", + "SDLK_LEFTPAREN", + "SDLK_RIGHTPAREN", + "SDLK_ASTERISK", + "SDLK_PLUS", + "SDLK_COMMA", + "SDLK_MINUS", + "SDLK_PERIOD", + "SDLK_SLASH", + "SDLK_0", + "SDLK_1", + "SDLK_2", + "SDLK_3", + "SDLK_4", + "SDLK_5", + "SDLK_6", + "SDLK_7", + "SDLK_8", + "SDLK_9", + "SDLK_COLON", + "SDLK_SEMICOLON", + "SDLK_LESS", + "SDLK_EQUALS", + "SDLK_GREATER", + "SDLK_QUESTION", + "SDLK_AT", + "SDLK_LEFTBRACKET", + "SDLK_BACKSLASH", + "SDLK_RIGHTBRACKET", + "SDLK_CARET", + "SDLK_UNDERSCORE", + "SDLK_BACKQUOTE", + "SDLK_a", + "SDLK_b", + "SDLK_c", + "SDLK_d", + "SDLK_e", + "SDLK_f", + "SDLK_g", + "SDLK_h", + "SDLK_i", + "SDLK_j", + "SDLK_k", + "SDLK_l", + "SDLK_m", + "SDLK_n", + "SDLK_o", + "SDLK_p", + "SDLK_q", + "SDLK_r", + "SDLK_s", + "SDLK_t", + "SDLK_u", + "SDLK_v", + "SDLK_w", + "SDLK_x", + "SDLK_y", + "SDLK_z", + "SDLK_CAPSLOCK", + "SDLK_F1", + "SDLK_F2", + "SDLK_F3", + "SDLK_F4", + "SDLK_F5", + "SDLK_F6", + "SDLK_F7", + "SDLK_F8", + "SDLK_F9", + "SDLK_F10", + "SDLK_F11", + "SDLK_F12", + "SDLK_PRINTSCREEN", + "SDLK_SCROLLLOCK", + "SDLK_PAUSE", + "SDLK_INSERT", + "SDLK_HOME", + "SDLK_PAGEUP", + "SDLK_DELETE", + "SDLK_END", + "SDLK_PAGEDOWN", + "SDLK_RIGHT", + "SDLK_LEFT", + "SDLK_DOWN", + "SDLK_UP", + "SDLK_NUMLOCKCLEAR", + "SDLK_KP_DIVIDE", + "SDLK_KP_MULTIPLY", + "SDLK_KP_MINUS", + "SDLK_KP_PLUS", + "SDLK_KP_ENTER", + "SDLK_KP_1", + "SDLK_KP_2", + "SDLK_KP_3", + "SDLK_KP_4", + "SDLK_KP_5", + "SDLK_KP_6", + "SDLK_KP_7", + "SDLK_KP_8", + "SDLK_KP_9", + "SDLK_KP_0", + "SDLK_KP_PERIOD", + "SDLK_APPLICATION", + "SDLK_POWER", + "SDLK_KP_EQUALS", + "SDLK_F13", + "SDLK_F14", + "SDLK_F15", + "SDLK_F16", + "SDLK_F17", + "SDLK_F18", + "SDLK_F19", + "SDLK_F20", + "SDLK_F21", + "SDLK_F22", + "SDLK_F23", + "SDLK_F24", + "SDLK_EXECUTE", + "SDLK_HELP", + "SDLK_MENU", + "SDLK_SELECT", + "SDLK_STOP", + "SDLK_AGAIN", + "SDLK_UNDO", + "SDLK_CUT", + "SDLK_COPY", + "SDLK_PASTE", + "SDLK_FIND", + "SDLK_MUTE", + "SDLK_VOLUMEUP", + "SDLK_VOLUMEDOWN", + "SDLK_KP_COMMA", + "SDLK_KP_EQUALSAS400", + "SDLK_ALTERASE", + "SDLK_SYSREQ", + "SDLK_CANCEL", + "SDLK_CLEAR", + "SDLK_PRIOR", + "SDLK_RETURN2", + "SDLK_SEPARATOR", + "SDLK_OUT", + "SDLK_OPER", + "SDLK_CLEARAGAIN", + "SDLK_CRSEL", + "SDLK_EXSEL", + "SDLK_KP_00", + "SDLK_KP_000", + "SDLK_THOUSANDSSEPARATOR", + "SDLK_DECIMALSEPARATOR", + "SDLK_CURRENCYUNIT", + "SDLK_CURRENCYSUBUNIT", + "SDLK_KP_LEFTPAREN", + "SDLK_KP_RIGHTPAREN", + "SDLK_KP_LEFTBRACE", + "SDLK_KP_RIGHTBRACE", + "SDLK_KP_TAB", + "SDLK_KP_BACKSPACE", + "SDLK_KP_A", + "SDLK_KP_B", + "SDLK_KP_C", + "SDLK_KP_D", + "SDLK_KP_E", + "SDLK_KP_F", + "SDLK_KP_XOR", + "SDLK_KP_POWER", + "SDLK_KP_PERCENT", + "SDLK_KP_LESS", + "SDLK_KP_GREATER", + "SDLK_KP_AMPERSAND", + "SDLK_KP_DBLAMPERSAND", + "SDLK_KP_VERTICALBAR", + "SDLK_KP_DBLVERTICALBAR", + "SDLK_KP_COLON", + "SDLK_KP_HASH", + "SDLK_KP_SPACE", + "SDLK_KP_AT", + "SDLK_KP_EXCLAM", + "SDLK_KP_MEMSTORE", + "SDLK_KP_MEMRECALL", + "SDLK_KP_MEMCLEAR", + "SDLK_KP_MEMADD", + "SDLK_KP_MEMSUBTRACT", + "SDLK_KP_MEMMULTIPLY", + "SDLK_KP_MEMDIVIDE", + "SDLK_KP_PLUSMINUS", + "SDLK_KP_CLEAR", + "SDLK_KP_CLEARENTRY", + "SDLK_KP_BINARY", + "SDLK_KP_OCTAL", + "SDLK_KP_DECIMAL", + "SDLK_KP_HEXADECIMAL", + "SDLK_LCTRL", + "SDLK_LSHIFT", + "SDLK_LALT", + "SDLK_LGUI", + "SDLK_RCTRL", + "SDLK_RSHIFT", + "SDLK_RALT", + "SDLK_RGUI", + "SDLK_MODE", + "SDLK_AUDIONEXT", + "SDLK_AUDIOPREV", + "SDLK_AUDIOSTOP", + "SDLK_AUDIOPLAY", + "SDLK_AUDIOMUTE", + "SDLK_MEDIASELECT", + "SDLK_WWW", + "SDLK_MAIL", + "SDLK_CALCULATOR", + "SDLK_COMPUTER", + "SDLK_AC_SEARCH", + "SDLK_AC_HOME", + "SDLK_AC_BACK", + "SDLK_AC_FORWARD", + "SDLK_AC_STOP", + "SDLK_AC_REFRESH", + "SDLK_AC_BOOKMARKS", + "SDLK_BRIGHTNESSDOWN", + "SDLK_BRIGHTNESSUP", + "SDLK_DISPLAYSWITCH", + "SDLK_KBDILLUMTOGGLE", + "SDLK_KBDILLUMDOWN", + "SDLK_KBDILLUMUP", + "SDLK_EJECT", + "SDLK_SLEEP", + "SDLK_APP1", + "SDLK_APP2", + "SDLK_AUDIOREWIND", + "SDLK_AUDIOFASTFORWARD", + "SDLK_SOFTLEFT", + "SDLK_SOFTRIGHT", + "SDLK_CALL", + "SDLK_ENDCALL", +) + +SDL_KEYMAP = {key: getattr(SDL_KeyCode, key) for key in SDL_KEYS} CONFIG_SCHEMA = ( binary_sensor.binary_sensor_schema(BinarySensor) diff --git a/tests/components/sdl/common.yaml b/tests/components/sdl/common.yaml index 50fa4a5990..52991d595c 100644 --- a/tests/components/sdl/common.yaml +++ b/tests/components/sdl/common.yaml @@ -14,10 +14,10 @@ display: binary_sensor: - platform: sdl id: key_up - key: SDLK_a + key: SDLK_UP - platform: sdl id: key_down - key: SDLK_d + key: SDLK_DOWN - platform: sdl id: key_enter - key: SDLK_s + key: SDLK_RETURN From 6c2f1c8a283165c8fe4ca66aa93317265c30d5a6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Nov 2025 01:53:27 -0500 Subject: [PATCH 357/394] wip action chaining --- esphome/core/automation.h | 1 + esphome/core/base_automation.h | 115 ++++++++++++++++++++++++++------- 2 files changed, 93 insertions(+), 23 deletions(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index aace7889f0..c22b3ca0e3 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -220,6 +220,7 @@ template class Action { protected: friend ActionList; + template friend class ContinuationAction; virtual void play(Ts... x) = 0; void play_next_(Ts... x) { diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 1c60dd1c7a..685f3ab8ae 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -171,9 +171,22 @@ template class DelayAction : public Action, public Compon TEMPLATABLE_VALUE(uint32_t, delay) void play_complex(Ts... x) override { - auto f = std::bind(&DelayAction::play_next_, this, x...); this->num_running_++; + // Store parameters in shared_ptr for this timer instance + // This avoids std::bind bloat while supporting parallel script mode + // shared_ptr is used (vs unique_ptr) because std::function requires copyability + auto params = std::make_shared>(x...); + + // Lambda captures only 'this' and the shared_ptr (8-16 bytes total) + // vs std::bind which captures 'this' + copies of all x... + bind overhead + // This eliminates ~200-300 bytes of std::bind template instantiation code + auto f = [this, params]() { + if (this->num_running_ > 0) { + std::apply([this](auto &&...args) { this->play_next_(args...); }, *params); + } + }; + // If num_running_ > 1, we have multiple instances running in parallel // In single/restart/queued modes, only one instance runs at a time // Parallel mode uses skip_cancel=true to allow multiple delays to coexist @@ -215,18 +228,46 @@ template class StatelessLambdaAction : public Action { void (*f_)(Ts...); }; +/// Simple continuation action that calls play_next_ on a parent action. +/// Used internally by IfAction, WhileAction, RepeatAction, etc. to chain actions. +/// Memory: 4-8 bytes (parent pointer) vs 40 bytes (LambdaAction with std::function). +template class ContinuationAction : public Action { + public: + explicit ContinuationAction(Action *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->play_next_(x...); } + + protected: + Action *parent_; +}; + +// Forward declaration for WhileLoopContinuation +template class WhileAction; + +/// Loop continuation for WhileAction that checks condition and repeats or continues. +/// Memory: 4-8 bytes (parent pointer) vs 40 bytes (LambdaAction with std::function). +template class WhileLoopContinuation : public Action { + public: + explicit WhileLoopContinuation(WhileAction *parent) : parent_(parent) {} + + void play(Ts... x) override; + + protected: + WhileAction *parent_; +}; + template class IfAction : public Action { public: explicit IfAction(Condition *condition) : condition_(condition) {} void add_then(const std::initializer_list *> &actions) { this->then_.add_actions(actions); - this->then_.add_action(new LambdaAction([this](Ts... x) { this->play_next_(x...); })); + this->then_.add_action(new ContinuationAction(this)); } void add_else(const std::initializer_list *> &actions) { this->else_.add_actions(actions); - this->else_.add_action(new LambdaAction([this](Ts... x) { this->play_next_(x...); })); + this->else_.add_action(new ContinuationAction(this)); } void play_complex(Ts... x) override { @@ -267,19 +308,11 @@ template class WhileAction : public Action { void add_then(const std::initializer_list *> &actions) { this->then_.add_actions(actions); - this->then_.add_action(new LambdaAction([this](Ts... x) { - if (this->num_running_ > 0 && this->condition_->check_tuple(this->var_)) { - // play again - if (this->num_running_ > 0) { - this->then_.play_tuple(this->var_); - } - } else { - // condition false, play next - this->play_next_tuple_(this->var_); - } - })); + this->then_.add_action(new WhileLoopContinuation(this)); } + friend class WhileLoopContinuation; + void play_complex(Ts... x) override { this->num_running_++; // Store loop parameters @@ -308,22 +341,45 @@ template class WhileAction : public Action { std::tuple var_{}; }; +// Implementation of WhileLoopContinuation::play +template void WhileLoopContinuation::play(Ts... x) { + if (this->parent_->num_running_ > 0 && this->parent_->condition_->check_tuple(this->parent_->var_)) { + // play again + if (this->parent_->num_running_ > 0) { + this->parent_->then_.play_tuple(this->parent_->var_); + } + } else { + // condition false, play next + this->parent_->play_next_tuple_(this->parent_->var_); + } +} + +// Forward declaration for RepeatLoopContinuation +template class RepeatAction; + +/// Loop continuation for RepeatAction that increments iteration and repeats or continues. +/// Memory: 4-8 bytes (parent pointer) vs 40 bytes (LambdaAction with std::function). +template class RepeatLoopContinuation : public Action { + public: + explicit RepeatLoopContinuation(RepeatAction *parent) : parent_(parent) {} + + void play(uint32_t iteration, Ts... x) override; + + protected: + RepeatAction *parent_; +}; + template class RepeatAction : public Action { public: TEMPLATABLE_VALUE(uint32_t, count) void add_then(const std::initializer_list *> &actions) { this->then_.add_actions(actions); - this->then_.add_action(new LambdaAction([this](uint32_t iteration, Ts... x) { - iteration++; - if (iteration >= this->count_.value(x...)) { - this->play_next_tuple_(this->var_); - } else { - this->then_.play(iteration, x...); - } - })); + this->then_.add_action(new RepeatLoopContinuation(this)); } + friend class RepeatLoopContinuation; + void play_complex(Ts... x) override { this->num_running_++; this->var_ = std::make_tuple(x...); @@ -344,6 +400,16 @@ template class RepeatAction : public Action { std::tuple var_; }; +// Implementation of RepeatLoopContinuation::play +template void RepeatLoopContinuation::play(uint32_t iteration, Ts... x) { + iteration++; + if (iteration >= this->parent_->count_.value(x...)) { + this->parent_->play_next_tuple_(this->parent_->var_); + } else { + this->parent_->then_.play(iteration, x...); + } +} + template class WaitUntilAction : public Action, public Component { public: WaitUntilAction(Condition *condition) : condition_(condition) {} @@ -362,7 +428,10 @@ template class WaitUntilAction : public Action, public Co this->var_ = std::make_tuple(x...); if (this->timeout_value_.has_value()) { - auto f = std::bind(&WaitUntilAction::play_next_, this, x...); + // Lambda captures only 'this' to reference stored var_ + // vs std::bind which duplicates storage of x... (already in var_) + // This eliminates ~100-200 bytes of std::bind template instantiation code + auto f = [this]() { this->play_next_tuple_(this->var_); }; this->set_timeout("timeout", this->timeout_value_.value(x...), f); } From e28c15229811ed6731ab446cdf880f0929555ac9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Nov 2025 04:48:58 -0500 Subject: [PATCH 358/394] [cpp_generator] Align isinstance() with codebase style (tuple vs PEP 604) (#11645) --- esphome/cpp_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index a2da424e5a..6f1af01a5b 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -350,7 +350,7 @@ def safe_exp(obj: SafeExpType) -> Expression: return IntLiteral(int(obj.total_seconds)) if isinstance(obj, TimePeriodMinutes): return IntLiteral(int(obj.total_minutes)) - if isinstance(obj, tuple | list): + if isinstance(obj, (tuple, list)): return ArrayInitializer(*[safe_exp(o) for o in obj]) if obj is bool: return bool_ From c662697ca77cca55e6b63a4515476f5404d5810f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Nov 2025 11:18:10 -0500 Subject: [PATCH 359/394] [json] Fix component test compilation errors (#11647) --- tests/components/json/common.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/components/json/common.yaml b/tests/components/json/common.yaml index f4074e1172..c36c7f2a5a 100644 --- a/tests/components/json/common.yaml +++ b/tests/components/json/common.yaml @@ -14,12 +14,14 @@ interval: // Test parse_json bool parse_ok = esphome::json::parse_json(json_str, [](JsonObject root) { - if (root.containsKey("sensor") && root.containsKey("value")) { + if (root["sensor"].is() && root["value"].is()) { const char* sensor = root["sensor"]; float value = root["value"]; ESP_LOGD("test", "Parsed: sensor=%s, value=%.1f", sensor, value); + return true; } else { ESP_LOGD("test", "Parsed JSON missing required keys"); + return false; } }); ESP_LOGD("test", "Parse result (JSON syntax only): %s", parse_ok ? "success" : "failed"); From 55af8186294b900d52cd31eabd20407f30563286 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Sat, 1 Nov 2025 17:18:38 +0100 Subject: [PATCH 360/394] [nrf52] fix compilation warning (#11656) --- esphome/components/zephyr/core.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index ad7a148cdb..365b6b8ed2 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -3,7 +3,7 @@ #include #include #include -#include +#include #include "esphome/core/hal.h" #include "esphome/core/helpers.h" From f502907c7abd8b6c43178860332984d0184dcb72 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Nov 2025 12:39:01 -0500 Subject: [PATCH 361/394] [web_server_idf] Reduce flash by eliminating temporary string allocations in event formatting --- .../web_server_idf/web_server_idf.cpp | 25 +++++++++++++------ .../web_server_idf/web_server_idf.h | 2 +- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index c3ba7ddc2b..85585383a7 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -348,7 +349,14 @@ void AsyncWebServerResponse::addHeader(const char *name, const char *value) { httpd_resp_set_hdr(*this->req_, name, value); } -void AsyncResponseStream::print(float value) { this->print(to_string(value)); } +void AsyncResponseStream::print(float value) { + // Use stack buffer to avoid temporary string allocation + // Size: sign (1) + digits (10) + decimal (1) + precision (6) + exponent (5) + null (1) = 24, use 32 for safety + constexpr size_t FLOAT_BUF_SIZE = 32; + char buf[FLOAT_BUF_SIZE]; + int len = snprintf(buf, FLOAT_BUF_SIZE, "%f", value); + this->content_.append(buf, len); +} void AsyncResponseStream::printf(const char *fmt, ...) { va_list args; @@ -594,16 +602,19 @@ bool AsyncEventSourceResponse::try_send_nodefer(const char *message, const char event_buffer_.append(chunk_len_header); + // Use stack buffer for formatting numeric fields to avoid temporary string allocations + // Size: "retry: " (7) + max uint32 (10 digits) + CRLF (2) + null (1) = 20 bytes, use 32 for safety + constexpr size_t NUM_BUF_SIZE = 32; + char num_buf[NUM_BUF_SIZE]; + if (reconnect) { - event_buffer_.append("retry: ", sizeof("retry: ") - 1); - event_buffer_.append(to_string(reconnect)); - event_buffer_.append(CRLF_STR, CRLF_LEN); + int len = snprintf(num_buf, NUM_BUF_SIZE, "retry: %" PRIu32 CRLF_STR, reconnect); + event_buffer_.append(num_buf, len); } if (id) { - event_buffer_.append("id: ", sizeof("id: ") - 1); - event_buffer_.append(to_string(id)); - event_buffer_.append(CRLF_STR, CRLF_LEN); + int len = snprintf(num_buf, NUM_BUF_SIZE, "id: %" PRIu32 CRLF_STR, id); + event_buffer_.append(num_buf, len); } if (event && *event) { diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 5ec6fec009..7f22bf264c 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -77,7 +77,7 @@ class AsyncResponseStream : public AsyncWebServerResponse { size_t get_content_size() const override { return this->content_.size(); }; void print(const char *str) { this->content_.append(str); } - void print(const std::string &str) { this->content_.append(str); } + void print(const std::string &str) { this->content_.append(str.c_str(), str.size()); } void print(float value); void printf(const char *fmt, ...) __attribute__((format(printf, 2, 3))); From 2f56af00786d233a29990ec03e4970a9eebe4a0f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Nov 2025 12:41:22 -0500 Subject: [PATCH 362/394] [web_server_idf] Reduce flash by eliminating temporary string allocations in event formatting --- esphome/components/web_server_idf/web_server_idf.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 7f22bf264c..5ec6fec009 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -77,7 +77,7 @@ class AsyncResponseStream : public AsyncWebServerResponse { size_t get_content_size() const override { return this->content_.size(); }; void print(const char *str) { this->content_.append(str); } - void print(const std::string &str) { this->content_.append(str.c_str(), str.size()); } + void print(const std::string &str) { this->content_.append(str); } void print(float value); void printf(const char *fmt, ...) __attribute__((format(printf, 2, 3))); From 0c101768d722285dc6246cf353270eb52ffd2f56 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Nov 2025 12:47:54 -0500 Subject: [PATCH 363/394] tests --- tests/components/web_server_idf/common.yaml | 29 +++++++++++++++++++ .../web_server_idf/test.esp32-idf.yaml | 3 ++ 2 files changed, 32 insertions(+) create mode 100644 tests/components/web_server_idf/common.yaml create mode 100644 tests/components/web_server_idf/test.esp32-idf.yaml diff --git a/tests/components/web_server_idf/common.yaml b/tests/components/web_server_idf/common.yaml new file mode 100644 index 0000000000..b1885af266 --- /dev/null +++ b/tests/components/web_server_idf/common.yaml @@ -0,0 +1,29 @@ +esphome: + name: test-web-server-idf + +esp32: + board: esp32dev + framework: + type: esp-idf + +network: + +# Add some entities to test SSE event formatting +sensor: + - platform: template + name: "Test Sensor" + id: test_sensor + update_interval: 60s + lambda: "return 42.5;" + +binary_sensor: + - platform: template + name: "Test Binary Sensor" + id: test_binary_sensor + lambda: "return true;" + +switch: + - platform: template + name: "Test Switch" + id: test_switch + optimistic: true diff --git a/tests/components/web_server_idf/test.esp32-idf.yaml b/tests/components/web_server_idf/test.esp32-idf.yaml new file mode 100644 index 0000000000..c3b85178ef --- /dev/null +++ b/tests/components/web_server_idf/test.esp32-idf.yaml @@ -0,0 +1,3 @@ +<<: !include common.yaml + +web_server: From afcce8e5c621e64822db3c4aa4781790093b5fa0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Nov 2025 13:01:18 -0500 Subject: [PATCH 364/394] fixup --- esphome/components/web_server_idf/web_server_idf.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 85585383a7..7b8b4e5699 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -246,8 +246,8 @@ void AsyncWebServerRequest::redirect(const std::string &url) { } void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type) { - // Set status code - use constants for common codes to avoid string allocation - const char *status = nullptr; + // Set status code - use constants for common codes, default to 500 for unknown codes + const char *status; switch (code) { case 200: status = HTTPD_200; @@ -259,9 +259,10 @@ void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code status = HTTPD_409; break; default: + status = HTTPD_500; break; } - httpd_resp_set_status(*this, status == nullptr ? to_string(code).c_str() : status); + httpd_resp_set_status(*this, status); if (content_type && *content_type) { httpd_resp_set_type(*this, content_type); From e91b0bb804f17efad3579122730d3b2447ad3326 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Nov 2025 13:13:56 -0500 Subject: [PATCH 365/394] preen --- .../components/web_server_idf/web_server_idf.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 7b8b4e5699..3f88f73f88 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -353,9 +353,9 @@ void AsyncWebServerResponse::addHeader(const char *name, const char *value) { void AsyncResponseStream::print(float value) { // Use stack buffer to avoid temporary string allocation // Size: sign (1) + digits (10) + decimal (1) + precision (6) + exponent (5) + null (1) = 24, use 32 for safety - constexpr size_t FLOAT_BUF_SIZE = 32; - char buf[FLOAT_BUF_SIZE]; - int len = snprintf(buf, FLOAT_BUF_SIZE, "%f", value); + constexpr size_t float_buf_size = 32; + char buf[float_buf_size]; + int len = snprintf(buf, float_buf_size, "%f", value); this->content_.append(buf, len); } @@ -605,16 +605,16 @@ bool AsyncEventSourceResponse::try_send_nodefer(const char *message, const char // Use stack buffer for formatting numeric fields to avoid temporary string allocations // Size: "retry: " (7) + max uint32 (10 digits) + CRLF (2) + null (1) = 20 bytes, use 32 for safety - constexpr size_t NUM_BUF_SIZE = 32; - char num_buf[NUM_BUF_SIZE]; + constexpr size_t num_buf_size = 32; + char num_buf[num_buf_size]; if (reconnect) { - int len = snprintf(num_buf, NUM_BUF_SIZE, "retry: %" PRIu32 CRLF_STR, reconnect); + int len = snprintf(num_buf, num_buf_size, "retry: %" PRIu32 CRLF_STR, reconnect); event_buffer_.append(num_buf, len); } if (id) { - int len = snprintf(num_buf, NUM_BUF_SIZE, "id: %" PRIu32 CRLF_STR, id); + int len = snprintf(num_buf, num_buf_size, "id: %" PRIu32 CRLF_STR, id); event_buffer_.append(num_buf, len); } From 66eb10cc55b6fc7c33d453bbb67ce432b87d90f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Nov 2025 14:52:45 -0500 Subject: [PATCH 366/394] fix ble latency --- esphome/components/esp32_ble/__init__.py | 7 ++ esphome/components/esp32_ble/ble.cpp | 115 +++++++++++++++++++++++ esphome/components/esp32_ble/ble.h | 14 +++ 3 files changed, 136 insertions(+) diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 411c2add71..beb6fd70da 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -7,6 +7,7 @@ from typing import Any from esphome import automation import esphome.codegen as cg +from esphome.components import socket from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant import esphome.config_validation as cv from esphome.const import ( @@ -481,6 +482,12 @@ async def to_code(config): cg.add(var.set_name(name)) await cg.register_component(var, config) + # BLE uses 1 UDP socket for event notification to wake up main loop from select() + # This enables low-latency (~12μs) BLE event processing instead of waiting for + # select() timeout (0-16ms). The socket is created in ble_setup_() and used to + # wake lwip_select() when BLE events arrive from the BLE thread. + socket.consume_sockets(1, "esp32_ble")(config) + # Define max connections for use in C++ code (e.g., ble_server.h) max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS) cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 5bbd5fe9ed..8730b894ea 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -27,6 +27,10 @@ extern "C" { #include #endif +#ifdef USE_SOCKET_SELECT_SUPPORT +#include +#endif + namespace esphome::esp32_ble { static const char *const TAG = "esp32_ble"; @@ -273,10 +277,21 @@ bool ESP32BLE::ble_setup_() { // BLE takes some time to be fully set up, 200ms should be more than enough delay(200); // NOLINT + // Set up notification socket to wake main loop for BLE events + // This enables low-latency (~12μs) event processing instead of waiting for select() timeout +#ifdef USE_SOCKET_SELECT_SUPPORT + this->setup_event_notification_(); +#endif + return true; } bool ESP32BLE::ble_dismantle_() { + // Clean up notification socket first before dismantling BLE stack +#ifdef USE_SOCKET_SELECT_SUPPORT + this->cleanup_event_notification_(); +#endif + esp_err_t err = esp_bluedroid_disable(); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err); @@ -374,6 +389,12 @@ void ESP32BLE::loop() { break; } +#ifdef USE_SOCKET_SELECT_SUPPORT + // Drain any notification socket events first + // This clears the socket so it doesn't stay "ready" in subsequent select() calls + this->drain_event_notifications_(); +#endif + BLEEvent *ble_event = this->ble_events_.pop(); while (ble_event != nullptr) { switch (ble_event->type_) { @@ -531,6 +552,12 @@ template void enqueue_ble_event(Args... args) { // Push the event to the queue global_ble->ble_events_.push(event); // Push always succeeds because we're the only producer and the pool ensures we never exceed queue size + + // Wake up main loop to process event immediately + // This is thread-safe - notify_main_loop_() uses lwip_sendto which is thread-safe +#ifdef USE_SOCKET_SELECT_SUPPORT + global_ble->notify_main_loop_(); +#endif } // Explicit template instantiations for the friend function @@ -630,6 +657,94 @@ void ESP32BLE::dump_config() { } } +#ifdef USE_SOCKET_SELECT_SUPPORT +void ESP32BLE::setup_event_notification_() { + // Guard against multiple calls (reentrant safety for ble.enable automation) + if (this->notify_fd_ >= 0) { + return; // Already set up + } + + // Create UDP socket for event notifications + this->notify_fd_ = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (this->notify_fd_ < 0) { + ESP_LOGW(TAG, "Event socket create failed: %d", errno); + return; + } + + // Bind to loopback with auto-assigned port + struct sockaddr_in addr = {}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = lwip_htonl(INADDR_LOOPBACK); + addr.sin_port = 0; // Auto-assign port + + if (lwip_bind(this->notify_fd_, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + ESP_LOGW(TAG, "Event socket bind failed: %d", errno); + lwip_close(this->notify_fd_); + this->notify_fd_ = -1; + return; + } + + // Get the assigned port for sendto() + socklen_t len = sizeof(this->notify_addr_); + if (lwip_getsockname(this->notify_fd_, (struct sockaddr *) &this->notify_addr_, &len) < 0) { + ESP_LOGW(TAG, "Event socket address failed: %d", errno); + lwip_close(this->notify_fd_); + this->notify_fd_ = -1; + return; + } + + // Set non-blocking mode + int flags = lwip_fcntl(this->notify_fd_, F_GETFL, 0); + lwip_fcntl(this->notify_fd_, F_SETFL, flags | O_NONBLOCK); + + // Register with application's select() loop + if (!App.register_socket_fd(this->notify_fd_)) { + ESP_LOGW(TAG, "Event socket register failed"); + lwip_close(this->notify_fd_); + this->notify_fd_ = -1; + return; + } + + ESP_LOGD(TAG, "Event socket ready"); +} + +void ESP32BLE::cleanup_event_notification_() { + // Guard against multiple calls (reentrant safety for ble.disable automation) + if (this->notify_fd_ < 0) { + return; // Already cleaned up + } + + App.unregister_socket_fd(this->notify_fd_); + lwip_close(this->notify_fd_); + this->notify_fd_ = -1; + ESP_LOGD(TAG, "Event socket closed"); +} + +void ESP32BLE::notify_main_loop_() { + // Called from BLE thread context when events are queued + // Wakes up lwip_select() in main loop by writing to loopback socket + if (this->notify_fd_ >= 0) { + const char dummy = 1; + // Non-blocking sendto - if it fails (unlikely), select() will wake on timeout anyway + // This is safe to call from BLE thread - sendto() is thread-safe in lwip + lwip_sendto(this->notify_fd_, &dummy, 1, 0, (struct sockaddr *) &this->notify_addr_, sizeof(this->notify_addr_)); + } +} + +void ESP32BLE::drain_event_notifications_() { + // Called from main loop to drain any pending notifications + // Must check is_socket_ready() to avoid blocking on empty socket + if (this->notify_fd_ >= 0 && App.is_socket_ready(this->notify_fd_)) { + char buffer[64]; + // Drain all pending notifications with non-blocking reads + // Multiple BLE events may have triggered multiple writes, so drain until EWOULDBLOCK + while (lwip_recvfrom(this->notify_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { + // Just draining, no action needed + } + } +} +#endif // USE_SOCKET_SELECT_SUPPORT + uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { uint64_t u = 0; u |= uint64_t(address[0] & 0xFF) << 40; diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index dc973f0e82..e03d7f4f03 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -162,6 +162,13 @@ class ESP32BLE : public Component { void advertising_init_(); #endif +#ifdef USE_SOCKET_SELECT_SUPPORT + void setup_event_notification_(); // Create notification socket + void cleanup_event_notification_(); // Close and unregister socket + void notify_main_loop_(); // Wake up select() from BLE thread + void drain_event_notifications_(); // Read pending notifications in main loop +#endif + private: template friend void enqueue_ble_event(Args... args); @@ -196,6 +203,13 @@ class ESP32BLE : public Component { esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum) uint32_t advertising_cycle_time_{}; // 4 bytes +#ifdef USE_SOCKET_SELECT_SUPPORT + // Event notification socket for waking up main loop from BLE thread + // Uses UDP loopback to wake lwip_select() with ~12μs latency vs 0-16ms timeout + struct sockaddr_in notify_addr_ {}; // 16 bytes (sockaddr_in structure) + int notify_fd_{-1}; // 4 bytes (file descriptor) +#endif + // 2-byte aligned members uint16_t appearance_{0}; // 2 bytes From 9c5dbd18c24b2757826f26ed1994b7f2263d0f8d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Nov 2025 14:53:12 -0500 Subject: [PATCH 367/394] fix ble latency --- esphome/components/esp32_ble/ble.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 8730b894ea..d73a54a973 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -659,11 +659,6 @@ void ESP32BLE::dump_config() { #ifdef USE_SOCKET_SELECT_SUPPORT void ESP32BLE::setup_event_notification_() { - // Guard against multiple calls (reentrant safety for ble.enable automation) - if (this->notify_fd_ >= 0) { - return; // Already set up - } - // Create UDP socket for event notifications this->notify_fd_ = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (this->notify_fd_ < 0) { From a29f209b46aebfd898b8c6809fabf3bf7818edd9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Nov 2025 14:53:34 -0500 Subject: [PATCH 368/394] fix ble latency --- esphome/components/esp32_ble/ble.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index d73a54a973..bdc0837a47 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -704,9 +704,8 @@ void ESP32BLE::setup_event_notification_() { } void ESP32BLE::cleanup_event_notification_() { - // Guard against multiple calls (reentrant safety for ble.disable automation) if (this->notify_fd_ < 0) { - return; // Already cleaned up + return; } App.unregister_socket_fd(this->notify_fd_); From f6a5a30dc283627e7655de037132b2d3f5dbf2b9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Nov 2025 14:55:37 -0500 Subject: [PATCH 369/394] fix ble latency --- esphome/components/esp32_ble/ble.cpp | 33 +++------------------ esphome/components/esp32_ble/ble.h | 43 +++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 33 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index bdc0837a47..cadb2cbc2a 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -704,39 +704,14 @@ void ESP32BLE::setup_event_notification_() { } void ESP32BLE::cleanup_event_notification_() { - if (this->notify_fd_ < 0) { - return; - } - - App.unregister_socket_fd(this->notify_fd_); - lwip_close(this->notify_fd_); - this->notify_fd_ = -1; - ESP_LOGD(TAG, "Event socket closed"); -} - -void ESP32BLE::notify_main_loop_() { - // Called from BLE thread context when events are queued - // Wakes up lwip_select() in main loop by writing to loopback socket if (this->notify_fd_ >= 0) { - const char dummy = 1; - // Non-blocking sendto - if it fails (unlikely), select() will wake on timeout anyway - // This is safe to call from BLE thread - sendto() is thread-safe in lwip - lwip_sendto(this->notify_fd_, &dummy, 1, 0, (struct sockaddr *) &this->notify_addr_, sizeof(this->notify_addr_)); + App.unregister_socket_fd(this->notify_fd_); + lwip_close(this->notify_fd_); + this->notify_fd_ = -1; + ESP_LOGD(TAG, "Event socket closed"); } } -void ESP32BLE::drain_event_notifications_() { - // Called from main loop to drain any pending notifications - // Must check is_socket_ready() to avoid blocking on empty socket - if (this->notify_fd_ >= 0 && App.is_socket_ready(this->notify_fd_)) { - char buffer[64]; - // Drain all pending notifications with non-blocking reads - // Multiple BLE events may have triggered multiple writes, so drain until EWOULDBLOCK - while (lwip_recvfrom(this->notify_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { - // Just draining, no action needed - } - } -} #endif // USE_SOCKET_SELECT_SUPPORT uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index e03d7f4f03..32f62baf88 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -25,6 +25,10 @@ #include #include +#ifdef USE_SOCKET_SELECT_SUPPORT +#include +#endif + namespace esphome::esp32_ble { // Maximum size of the BLE event queue @@ -163,10 +167,10 @@ class ESP32BLE : public Component { #endif #ifdef USE_SOCKET_SELECT_SUPPORT - void setup_event_notification_(); // Create notification socket - void cleanup_event_notification_(); // Close and unregister socket - void notify_main_loop_(); // Wake up select() from BLE thread - void drain_event_notifications_(); // Read pending notifications in main loop + void setup_event_notification_(); // Create notification socket + void cleanup_event_notification_(); // Close and unregister socket + inline void notify_main_loop_(); // Wake up select() from BLE thread (hot path - inlined) + inline void drain_event_notifications_(); // Read pending notifications in main loop (hot path - inlined) #endif private: @@ -221,6 +225,37 @@ class ESP32BLE : public Component { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern ESP32BLE *global_ble; +#ifdef USE_SOCKET_SELECT_SUPPORT +// Inline implementations for hot-path functions +// These are called from BLE thread (notify) and main loop (drain) on every event + +inline void ESP32BLE::notify_main_loop_() { + // Called from BLE thread context when events are queued + // Wakes up lwip_select() in main loop by writing to loopback socket + if (this->notify_fd_ >= 0) { + const char dummy = 1; + // Non-blocking sendto - if it fails (unlikely), select() will wake on timeout anyway + // This is safe to call from BLE thread - sendto() is thread-safe in lwip + lwip_sendto(this->notify_fd_, &dummy, 1, 0, (struct sockaddr *) &this->notify_addr_, sizeof(this->notify_addr_)); + } +} + +inline void ESP32BLE::drain_event_notifications_() { + // Called from main loop to drain any pending notifications + // Must check is_socket_ready() to avoid blocking on empty socket + // Requires App to be defined - include esphome/core/application.h in .cpp files that use this + extern esphome::Application App; + if (this->notify_fd_ >= 0 && App.is_socket_ready(this->notify_fd_)) { + char buffer[16]; + // Drain all pending notifications with non-blocking reads + // Multiple BLE events may have triggered multiple writes, so drain until EWOULDBLOCK + while (lwip_recvfrom(this->notify_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { + // Just draining, no action needed + } + } +} +#endif // USE_SOCKET_SELECT_SUPPORT + template class BLEEnabledCondition : public Condition { public: bool check(Ts... x) override { return global_ble->is_active(); } From ff2e2bed666c582e5b3ff356b413224f36d4a987 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Nov 2025 14:56:11 -0500 Subject: [PATCH 370/394] fix ble latency --- esphome/components/esp32_ble/ble.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 32f62baf88..b62109bff5 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -229,6 +229,10 @@ extern ESP32BLE *global_ble; // Inline implementations for hot-path functions // These are called from BLE thread (notify) and main loop (drain) on every event +// Small buffer for draining notification bytes (1 byte sent per BLE event) +// Size allows draining multiple notifications per recvfrom() without wasting stack +static constexpr size_t BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE = 16; + inline void ESP32BLE::notify_main_loop_() { // Called from BLE thread context when events are queued // Wakes up lwip_select() in main loop by writing to loopback socket @@ -246,7 +250,7 @@ inline void ESP32BLE::drain_event_notifications_() { // Requires App to be defined - include esphome/core/application.h in .cpp files that use this extern esphome::Application App; if (this->notify_fd_ >= 0 && App.is_socket_ready(this->notify_fd_)) { - char buffer[16]; + char buffer[BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE]; // Drain all pending notifications with non-blocking reads // Multiple BLE events may have triggered multiple writes, so drain until EWOULDBLOCK while (lwip_recvfrom(this->notify_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { From 69af4cddb5f6d1b315d76eb75444174cc819799d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Nov 2025 14:58:24 -0500 Subject: [PATCH 371/394] fix ble latency --- esphome/components/esp32_ble/ble.cpp | 17 ++++++++++++++--- esphome/components/esp32_ble/ble.h | 15 ++++++++------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index cadb2cbc2a..7298dc9621 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -679,15 +679,26 @@ void ESP32BLE::setup_event_notification_() { return; } - // Get the assigned port for sendto() - socklen_t len = sizeof(this->notify_addr_); - if (lwip_getsockname(this->notify_fd_, (struct sockaddr *) &this->notify_addr_, &len) < 0) { + // Get the assigned address and connect to it + // Connecting a UDP socket allows using send() instead of sendto() for better performance + struct sockaddr_in notify_addr; + socklen_t len = sizeof(notify_addr); + if (lwip_getsockname(this->notify_fd_, (struct sockaddr *) ¬ify_addr, &len) < 0) { ESP_LOGW(TAG, "Event socket address failed: %d", errno); lwip_close(this->notify_fd_); this->notify_fd_ = -1; return; } + // Connect to self (loopback) - allows using send() instead of sendto() + // After connect(), no need to store notify_addr - the socket remembers it + if (lwip_connect(this->notify_fd_, (struct sockaddr *) ¬ify_addr, sizeof(notify_addr)) < 0) { + ESP_LOGW(TAG, "Event socket connect failed: %d", errno); + lwip_close(this->notify_fd_); + this->notify_fd_ = -1; + return; + } + // Set non-blocking mode int flags = lwip_fcntl(this->notify_fd_, F_GETFL, 0); lwip_fcntl(this->notify_fd_, F_SETFL, flags | O_NONBLOCK); diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index b62109bff5..93c03063c6 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -209,9 +209,9 @@ class ESP32BLE : public Component { #ifdef USE_SOCKET_SELECT_SUPPORT // Event notification socket for waking up main loop from BLE thread - // Uses UDP loopback to wake lwip_select() with ~12μs latency vs 0-16ms timeout - struct sockaddr_in notify_addr_ {}; // 16 bytes (sockaddr_in structure) - int notify_fd_{-1}; // 4 bytes (file descriptor) + // Uses connected UDP loopback socket to wake lwip_select() with ~12μs latency vs 0-16ms timeout + // Socket is connected during setup, allowing use of send() instead of sendto() for efficiency + int notify_fd_{-1}; // 4 bytes (file descriptor) #endif // 2-byte aligned members @@ -235,12 +235,13 @@ static constexpr size_t BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE = 16; inline void ESP32BLE::notify_main_loop_() { // Called from BLE thread context when events are queued - // Wakes up lwip_select() in main loop by writing to loopback socket + // Wakes up lwip_select() in main loop by writing to connected loopback socket if (this->notify_fd_ >= 0) { const char dummy = 1; - // Non-blocking sendto - if it fails (unlikely), select() will wake on timeout anyway - // This is safe to call from BLE thread - sendto() is thread-safe in lwip - lwip_sendto(this->notify_fd_, &dummy, 1, 0, (struct sockaddr *) &this->notify_addr_, sizeof(this->notify_addr_)); + // Non-blocking send - if it fails (unlikely), select() will wake on timeout anyway + // This is safe to call from BLE thread - send() is thread-safe in lwip + // Socket is already connected to loopback address, so send() is faster than sendto() + lwip_send(this->notify_fd_, &dummy, 1, 0); } } From 32ea82060d5e134577bbfaf26531b885dc096c75 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Nov 2025 15:02:26 -0500 Subject: [PATCH 372/394] fix ble latency --- esphome/components/esp32_ble/ble.cpp | 13 +++++++++++++ esphome/components/esp32_ble/ble.h | 12 +++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 7298dc9621..797dbcc2bf 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -723,6 +723,19 @@ void ESP32BLE::cleanup_event_notification_() { } } +void ESP32BLE::drain_event_notifications_() { + // Called from main loop to drain any pending notifications + // Must check is_socket_ready() to avoid blocking on empty socket + if (this->notify_fd_ >= 0 && App.is_socket_ready(this->notify_fd_)) { + char buffer[BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE]; + // Drain all pending notifications with non-blocking reads + // Multiple BLE events may have triggered multiple writes, so drain until EWOULDBLOCK + while (lwip_recvfrom(this->notify_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { + // Just draining, no action needed + } + } +} + #endif // USE_SOCKET_SELECT_SUPPORT uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 93c03063c6..a91d8756a8 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -167,10 +167,10 @@ class ESP32BLE : public Component { #endif #ifdef USE_SOCKET_SELECT_SUPPORT - void setup_event_notification_(); // Create notification socket - void cleanup_event_notification_(); // Close and unregister socket - inline void notify_main_loop_(); // Wake up select() from BLE thread (hot path - inlined) - inline void drain_event_notifications_(); // Read pending notifications in main loop (hot path - inlined) + void setup_event_notification_(); // Create notification socket + void cleanup_event_notification_(); // Close and unregister socket + inline void notify_main_loop_(); // Wake up select() from BLE thread (hot path - inlined) + void drain_event_notifications_(); // Read pending notifications in main loop #endif private: @@ -248,9 +248,7 @@ inline void ESP32BLE::notify_main_loop_() { inline void ESP32BLE::drain_event_notifications_() { // Called from main loop to drain any pending notifications // Must check is_socket_ready() to avoid blocking on empty socket - // Requires App to be defined - include esphome/core/application.h in .cpp files that use this - extern esphome::Application App; - if (this->notify_fd_ >= 0 && App.is_socket_ready(this->notify_fd_)) { + if (this->notify_fd_ >= 0 && esphome::App.is_socket_ready(this->notify_fd_)) { char buffer[BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE]; // Drain all pending notifications with non-blocking reads // Multiple BLE events may have triggered multiple writes, so drain until EWOULDBLOCK From b80f40676a82e88f124abf6289117f7fc478c8a4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Nov 2025 15:02:51 -0500 Subject: [PATCH 373/394] fix ble latency --- esphome/components/esp32_ble/ble.h | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index a91d8756a8..facb0e5853 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -244,19 +244,6 @@ inline void ESP32BLE::notify_main_loop_() { lwip_send(this->notify_fd_, &dummy, 1, 0); } } - -inline void ESP32BLE::drain_event_notifications_() { - // Called from main loop to drain any pending notifications - // Must check is_socket_ready() to avoid blocking on empty socket - if (this->notify_fd_ >= 0 && esphome::App.is_socket_ready(this->notify_fd_)) { - char buffer[BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE]; - // Drain all pending notifications with non-blocking reads - // Multiple BLE events may have triggered multiple writes, so drain until EWOULDBLOCK - while (lwip_recvfrom(this->notify_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { - // Just draining, no action needed - } - } -} #endif // USE_SOCKET_SELECT_SUPPORT template class BLEEnabledCondition : public Condition { From bb2418a53f1e44de7e731add6d23bf264fe96c89 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Nov 2025 15:13:30 -0500 Subject: [PATCH 374/394] fix --- esphome/components/esp32_ble/ble.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 797dbcc2bf..2fcc9270cd 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -552,12 +552,6 @@ template void enqueue_ble_event(Args... args) { // Push the event to the queue global_ble->ble_events_.push(event); // Push always succeeds because we're the only producer and the pool ensures we never exceed queue size - - // Wake up main loop to process event immediately - // This is thread-safe - notify_main_loop_() uses lwip_sendto which is thread-safe -#ifdef USE_SOCKET_SELECT_SUPPORT - global_ble->notify_main_loop_(); -#endif } // Explicit template instantiations for the friend function @@ -611,6 +605,10 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { enqueue_ble_event(event, gatts_if, param); + // Wake up main loop to process GATT event immediately +#ifdef USE_SOCKET_SELECT_SUPPORT + global_ble->notify_main_loop_(); +#endif } #endif @@ -618,6 +616,10 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { enqueue_ble_event(event, gattc_if, param); + // Wake up main loop to process GATT event immediately +#ifdef USE_SOCKET_SELECT_SUPPORT + global_ble->notify_main_loop_(); +#endif } #endif From 604508e3d8fd9e006eba464096139762fc5e8358 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Nov 2025 15:23:35 -0500 Subject: [PATCH 375/394] fix --- esphome/components/esp32_ble/__init__.py | 2 ++ esphome/components/esp32_ble/ble.cpp | 4 +++- esphome/components/esp32_ble/ble.h | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index beb6fd70da..1ae8df6f5e 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -486,6 +486,8 @@ async def to_code(config): # This enables low-latency (~12μs) BLE event processing instead of waiting for # select() timeout (0-16ms). The socket is created in ble_setup_() and used to # wake lwip_select() when BLE events arrive from the BLE thread. + # Note: Called during config generation, socket is created at runtime. In practice, + # always used since esp32_ble only runs on ESP32 which always has USE_SOCKET_SELECT_SUPPORT. socket.consume_sockets(1, "esp32_ble")(config) # Define max connections for use in C++ code (e.g., ble_server.h) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 2fcc9270cd..9cb482bcbb 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -732,8 +732,10 @@ void ESP32BLE::drain_event_notifications_() { char buffer[BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE]; // Drain all pending notifications with non-blocking reads // Multiple BLE events may have triggered multiple writes, so drain until EWOULDBLOCK + // We control both ends of this loopback socket (always write 1 byte per event), + // so no error checking needed - any errors indicate catastrophic system failure while (lwip_recvfrom(this->notify_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { - // Just draining, no action needed + // Just draining, no action needed - actual BLE events are already queued } } } diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index facb0e5853..7c3195db6d 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -239,6 +239,8 @@ inline void ESP32BLE::notify_main_loop_() { if (this->notify_fd_ >= 0) { const char dummy = 1; // Non-blocking send - if it fails (unlikely), select() will wake on timeout anyway + // No error checking needed: we control both ends of this loopback socket, and the + // BLE event is already queued. Notification is best-effort to reduce latency. // This is safe to call from BLE thread - send() is thread-safe in lwip // Socket is already connected to loopback address, so send() is faster than sendto() lwip_send(this->notify_fd_, &dummy, 1, 0); From e2e20d79d092c16e93f5ef56808119bfe0cb299a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Nov 2025 16:58:17 -0500 Subject: [PATCH 376/394] [core] Remove redundant fd bounds check in yield_with_select_() --- esphome/core/application.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index c745aa0ae5..a16a6c851a 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -576,10 +576,9 @@ void Application::yield_with_select_(uint32_t delay_ms) { // Update fd_set if socket list has changed if (this->socket_fds_changed_) { FD_ZERO(&this->base_read_fds_); + // fd bounds are already validated in register_socket_fd() for (int fd : this->socket_fds_) { - if (fd >= 0 && fd < FD_SETSIZE) { - FD_SET(fd, &this->base_read_fds_); - } + FD_SET(fd, &this->base_read_fds_); } this->socket_fds_changed_ = false; } From b97c688f25cd78e689f08058ffa1f6789a5437a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Nov 2025 18:31:26 -0500 Subject: [PATCH 377/394] [api] Remove unnecessary intermediate variable in frame helpers --- esphome/components/api/api_frame_helper_noise.cpp | 3 +-- esphome/components/api/api_frame_helper_plaintext.cpp | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index e952ea670b..633b07a7fa 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -434,8 +434,7 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st return APIError::OK; } - std::vector *raw_buffer = buffer.get_buffer(); - uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer + uint8_t *buffer_data = buffer.get_buffer()->data(); this->reusable_iovs_.clear(); this->reusable_iovs_.reserve(packets.size()); diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 471e6c5404..dcbd35aa32 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -230,8 +230,7 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer return APIError::OK; } - std::vector *raw_buffer = buffer.get_buffer(); - uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer + uint8_t *buffer_data = buffer.get_buffer()->data(); this->reusable_iovs_.clear(); this->reusable_iovs_.reserve(packets.size()); From d25121a55c100da3aa3ddce112beac9e43f5b2e3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Nov 2025 22:43:08 -0500 Subject: [PATCH 378/394] [core] Remove redundant fd bounds check in yield_with_select_() (#11666) --- esphome/core/application.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index c745aa0ae5..61cfcc7585 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -576,10 +576,11 @@ void Application::yield_with_select_(uint32_t delay_ms) { // Update fd_set if socket list has changed if (this->socket_fds_changed_) { FD_ZERO(&this->base_read_fds_); + // fd bounds are already validated in register_socket_fd() or guaranteed by platform design: + // - ESP32: LwIP guarantees fd < FD_SETSIZE by design (LWIP_SOCKET_OFFSET = FD_SETSIZE - CONFIG_LWIP_MAX_SOCKETS) + // - Other platforms: register_socket_fd() validates fd < FD_SETSIZE for (int fd : this->socket_fds_) { - if (fd >= 0 && fd < FD_SETSIZE) { - FD_SET(fd, &this->base_read_fds_); - } + FD_SET(fd, &this->base_read_fds_); } this->socket_fds_changed_ = false; } From 1fc3165b582a290f65b5be68a52ee45d75721ca5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Nov 2025 22:43:39 -0500 Subject: [PATCH 379/394] [api] Remove unnecessary intermediate variable in frame helpers (#11668) --- esphome/components/api/api_frame_helper_noise.cpp | 3 +-- esphome/components/api/api_frame_helper_plaintext.cpp | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index e952ea670b..633b07a7fa 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -434,8 +434,7 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st return APIError::OK; } - std::vector *raw_buffer = buffer.get_buffer(); - uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer + uint8_t *buffer_data = buffer.get_buffer()->data(); this->reusable_iovs_.clear(); this->reusable_iovs_.reserve(packets.size()); diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 471e6c5404..dcbd35aa32 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -230,8 +230,7 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer return APIError::OK; } - std::vector *raw_buffer = buffer.get_buffer(); - uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer + uint8_t *buffer_data = buffer.get_buffer()->data(); this->reusable_iovs_.clear(); this->reusable_iovs_.reserve(packets.size()); From edde2fc94c584cb07365c16c6fe7a9d0b20488a1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 08:18:17 -0600 Subject: [PATCH 380/394] Add basic tests for web_server_idf (#11659) --- tests/components/web_server_idf/common.yaml | 29 +++++++++++++++++++ .../web_server_idf/test.esp32-idf.yaml | 3 ++ 2 files changed, 32 insertions(+) create mode 100644 tests/components/web_server_idf/common.yaml create mode 100644 tests/components/web_server_idf/test.esp32-idf.yaml diff --git a/tests/components/web_server_idf/common.yaml b/tests/components/web_server_idf/common.yaml new file mode 100644 index 0000000000..b1885af266 --- /dev/null +++ b/tests/components/web_server_idf/common.yaml @@ -0,0 +1,29 @@ +esphome: + name: test-web-server-idf + +esp32: + board: esp32dev + framework: + type: esp-idf + +network: + +# Add some entities to test SSE event formatting +sensor: + - platform: template + name: "Test Sensor" + id: test_sensor + update_interval: 60s + lambda: "return 42.5;" + +binary_sensor: + - platform: template + name: "Test Binary Sensor" + id: test_binary_sensor + lambda: "return true;" + +switch: + - platform: template + name: "Test Switch" + id: test_switch + optimistic: true diff --git a/tests/components/web_server_idf/test.esp32-idf.yaml b/tests/components/web_server_idf/test.esp32-idf.yaml new file mode 100644 index 0000000000..c3b85178ef --- /dev/null +++ b/tests/components/web_server_idf/test.esp32-idf.yaml @@ -0,0 +1,3 @@ +<<: !include common.yaml + +web_server: From f6946c0b9aed3b15986079c16c98c0e51438311a Mon Sep 17 00:00:00 2001 From: Kjell Braden Date: Sun, 2 Nov 2025 22:08:45 +0100 Subject: [PATCH 381/394] add integration test for script re-entry argument issue (#11652) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .../fixtures/action_concurrent_reentry.yaml | 105 ++++++++++++++++++ .../test_action_concurrent_reentry.py | 93 ++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 tests/integration/fixtures/action_concurrent_reentry.yaml create mode 100644 tests/integration/test_action_concurrent_reentry.py diff --git a/tests/integration/fixtures/action_concurrent_reentry.yaml b/tests/integration/fixtures/action_concurrent_reentry.yaml new file mode 100644 index 0000000000..68d36d1510 --- /dev/null +++ b/tests/integration/fixtures/action_concurrent_reentry.yaml @@ -0,0 +1,105 @@ +esphome: + name: action-concurrent-reentry + on_boot: + - priority: -100 + then: + - repeat: + count: 5 + then: + - lambda: id(handler_wait_until)->execute(id(global_counter)); + - lambda: id(handler_repeat)->execute(id(global_counter)); + - lambda: id(handler_while)->execute(id(global_counter)); + - lambda: id(handler_script_wait)->execute(id(global_counter)); + - delay: 50ms + - lambda: id(global_counter)++; + - delay: 50ms + +host: + +api: + +globals: + - id: global_counter + type: int + +script: + - id: handler_wait_until + + mode: parallel + + parameters: + arg: int + + then: + - wait_until: + condition: + lambda: return id(global_counter) == 5; + + - logger.log: + format: "AFTER wait_until ARG %d" + args: + - arg + + - id: handler_script_wait + + mode: parallel + + parameters: + arg: int + + then: + - script.wait: handler_wait_until + + - logger.log: + format: "AFTER script.wait ARG %d" + args: + - arg + + - id: handler_repeat + + mode: parallel + + parameters: + arg: int + + then: + - repeat: + count: 3 + then: + - logger.log: + format: "IN repeat %d ARG %d" + args: + - iteration + - arg + - delay: 100ms + + - logger.log: + format: "AFTER repeat ARG %d" + args: + - arg + + - id: handler_while + + mode: parallel + + parameters: + arg: int + + then: + - while: + condition: + lambda: return id(global_counter) != 5; + then: + - logger.log: + format: "IN while ARG %d" + args: + - arg + - delay: 100ms + + - logger.log: + format: "AFTER while ARG %d" + args: + - arg + +logger: + level: DEBUG diff --git a/tests/integration/test_action_concurrent_reentry.py b/tests/integration/test_action_concurrent_reentry.py new file mode 100644 index 0000000000..ba67e4c798 --- /dev/null +++ b/tests/integration/test_action_concurrent_reentry.py @@ -0,0 +1,93 @@ +"""Integration test for API conditional memory optimization with triggers and services.""" + +from __future__ import annotations + +import asyncio +import collections +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.xfail(reason="https://github.com/esphome/issues/issues/6534") +@pytest.mark.asyncio +async def test_action_concurrent_reentry( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """ + This test runs a script in parallel with varying arguments and verifies if + each script keeps its original argument throughout its execution + """ + test_complete = asyncio.Event() + expected = {0, 1, 2, 3, 4} + + # Patterns to match in logs + after_wait_until_pattern = re.compile(r"AFTER wait_until ARG (\d+)") + after_script_wait_pattern = re.compile(r"AFTER script\.wait ARG (\d+)") + after_repeat_pattern = re.compile(r"AFTER repeat ARG (\d+)") + in_repeat_pattern = re.compile(r"IN repeat (\d+) ARG (\d+)") + after_while_pattern = re.compile(r"AFTER while ARG (\d+)") + in_while_pattern = re.compile(r"IN while ARG (\d+)") + + after_wait_until_args = [] + after_script_wait_args = [] + after_while_args = [] + in_while_args = [] + after_repeat_args = [] + in_repeat_args = collections.defaultdict(list) + + def check_output(line: str) -> None: + """Check log output for expected messages.""" + if test_complete.is_set(): + return + + if mo := after_wait_until_pattern.search(line): + after_wait_until_args.append(int(mo.group(1))) + elif mo := after_script_wait_pattern.search(line): + after_script_wait_args.append(int(mo.group(1))) + elif mo := in_while_pattern.search(line): + in_while_args.append(int(mo.group(1))) + elif mo := after_while_pattern.search(line): + after_while_args.append(int(mo.group(1))) + elif mo := in_repeat_pattern.search(line): + in_repeat_args[int(mo.group(1))].append(int(mo.group(2))) + elif mo := after_repeat_pattern.search(line): + after_repeat_args.append(int(mo.group(1))) + if len(after_repeat_args) == len(expected): + test_complete.set() + + # Run with log monitoring + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "action-concurrent-reentry" + + # Wait for tests to complete with timeout + try: + await asyncio.wait_for(test_complete.wait(), timeout=8.0) + except TimeoutError: + pytest.fail("test timed out") + + # order may change, but all args must be present + for args in in_repeat_args.values(): + assert set(args) == expected + assert set(in_repeat_args.keys()) == {0, 1, 2} + assert set(after_wait_until_args) == expected, after_wait_until_args + assert set(after_script_wait_args) == expected, after_script_wait_args + assert set(after_repeat_args) == expected, after_repeat_args + assert set(after_while_args) == expected, after_while_args + assert dict(collections.Counter(in_while_args)) == { + 0: 5, + 1: 4, + 2: 3, + 3: 2, + 4: 1, + }, in_while_args From 425c88ee9434f35fcb093488f08a5651c4df57cf Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Sun, 2 Nov 2025 23:06:13 +0100 Subject: [PATCH 382/394] [nextion] Send `auto_wake_on_touch` as part of startup commands on loop (#11670) --- esphome/components/nextion/nextion.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index fc152ece1e..d77af510d7 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -323,6 +323,8 @@ void Nextion::loop() { this->set_touch_sleep_timeout(this->touch_sleep_timeout_); } + this->set_auto_wake_on_touch(this->connection_state_.auto_wake_on_touch_); + this->connection_state_.ignore_is_setup_ = false; } From 338190abeca6cd7cd29ca6065065d549c490857e Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Sun, 2 Nov 2025 19:11:02 -0300 Subject: [PATCH 383/394] ESP32 Pin loopTask to CORE 1 (#11669) --- esphome/components/esp32/core.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 3427c96e70..1c8f29fa95 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -96,7 +96,11 @@ void loop_task(void *pv_params) { extern "C" void app_main() { esp32::setup_preferences(); +#if CONFIG_FREERTOS_UNICORE xTaskCreate(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle); +#else + xTaskCreatePinnedToCore(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle, 1); +#endif } #endif // USE_ESP_IDF From 70ea3af578e1440fc49751dff420bc41f195af20 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Sun, 2 Nov 2025 23:19:28 +0100 Subject: [PATCH 384/394] [nrf52,gpio] switch input gpio to polling mode (#11664) --- esphome/components/gpio/binary_sensor/__init__.py | 3 ++- esphome/components/zephyr/gpio.cpp | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/esphome/components/gpio/binary_sensor/__init__.py b/esphome/components/gpio/binary_sensor/__init__.py index ca4dc43e9c..3c2021d40e 100644 --- a/esphome/components/gpio/binary_sensor/__init__.py +++ b/esphome/components/gpio/binary_sensor/__init__.py @@ -39,6 +39,7 @@ CONFIG_SCHEMA = ( # due to hardware limitations or lack of reliable interrupt support. This ensures # stable operation on these platforms. Future maintainers should verify platform # capabilities before changing this default behavior. + # nrf52 has no gpio interrupts implemented yet cv.SplitDefault( CONF_USE_INTERRUPT, bk72xx=False, @@ -46,7 +47,7 @@ CONFIG_SCHEMA = ( esp8266=True, host=True, ln882x=False, - nrf52=True, + nrf52=False, rp2040=True, rtl87xx=False, ): cv.boolean, diff --git a/esphome/components/zephyr/gpio.cpp b/esphome/components/zephyr/gpio.cpp index 4b84910368..41b983535c 100644 --- a/esphome/components/zephyr/gpio.cpp +++ b/esphome/components/zephyr/gpio.cpp @@ -8,8 +8,8 @@ namespace zephyr { static const char *const TAG = "zephyr"; -static int flags_to_mode(gpio::Flags flags, bool inverted, bool value) { - int ret = 0; +static gpio_flags_t flags_to_mode(gpio::Flags flags, bool inverted, bool value) { + gpio_flags_t ret = 0; if (flags & gpio::FLAG_INPUT) { ret |= GPIO_INPUT; } @@ -79,7 +79,10 @@ void ZephyrGPIOPin::pin_mode(gpio::Flags flags) { if (nullptr == this->gpio_) { return; } - gpio_pin_configure(this->gpio_, this->pin_ % 32, flags_to_mode(flags, this->inverted_, this->value_)); + auto ret = gpio_pin_configure(this->gpio_, this->pin_ % 32, flags_to_mode(flags, this->inverted_, this->value_)); + if (ret != 0) { + ESP_LOGE(TAG, "gpio %u cannot be configured %d.", this->pin_, ret); + } } std::string ZephyrGPIOPin::dump_summary() const { From 50e7ce55e7370f4f015488e0e61e2746134d7075 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Sun, 2 Nov 2025 23:20:30 +0100 Subject: [PATCH 385/394] [nrf52] enable nrf52 test (#11379) --- esphome/components/nrf52/__init__.py | 1 + tests/components/nrf52/test-bootloader.nrf52-xiao-ble.yaml | 3 +++ tests/components/nrf52/test.nrf52-mcumgr.yaml | 0 3 files changed, 4 insertions(+) create mode 100644 tests/components/nrf52/test-bootloader.nrf52-xiao-ble.yaml create mode 100644 tests/components/nrf52/test.nrf52-mcumgr.yaml diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 27e1246744..ace324c1f5 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -290,6 +290,7 @@ def show_logs(config: ConfigType, args, devices: list[str]) -> bool: address = ble_device.address else: return True + if is_mac_address(address): asyncio.run(logger_connect(address)) return True diff --git a/tests/components/nrf52/test-bootloader.nrf52-xiao-ble.yaml b/tests/components/nrf52/test-bootloader.nrf52-xiao-ble.yaml new file mode 100644 index 0000000000..022ab9c753 --- /dev/null +++ b/tests/components/nrf52/test-bootloader.nrf52-xiao-ble.yaml @@ -0,0 +1,3 @@ +nrf52: + # it is not correct bootloader for the board + bootloader: adafruit_nrf52_sd140_v6 diff --git a/tests/components/nrf52/test.nrf52-mcumgr.yaml b/tests/components/nrf52/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..e69de29bb2 From c822ec152f45528949dc9b220165774286cf7456 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Sun, 2 Nov 2025 23:22:49 +0100 Subject: [PATCH 386/394] Enable IPv6 for host (#11630) --- esphome/components/network/__init__.py | 2 ++ tests/components/network/test-ipv6.host.yaml | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 tests/components/network/test-ipv6.host.yaml diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 502803da1e..1d62b661ca 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -55,6 +55,7 @@ CONFIG_SCHEMA = cv.Schema( esp32=False, rp2040=False, bk72xx=False, + host=False, ): cv.All( cv.boolean, cv.Any( @@ -64,6 +65,7 @@ CONFIG_SCHEMA = cv.Schema( esp8266_arduino=cv.Version(0, 0, 0), rp2040_arduino=cv.Version(0, 0, 0), bk72xx_arduino=cv.Version(1, 7, 0), + host=cv.Version(0, 0, 0), ), cv.boolean_false, ), diff --git a/tests/components/network/test-ipv6.host.yaml b/tests/components/network/test-ipv6.host.yaml new file mode 100644 index 0000000000..d9eeab89ea --- /dev/null +++ b/tests/components/network/test-ipv6.host.yaml @@ -0,0 +1,2 @@ +network: + enable_ipv6: true From 79378a930e8d10871a765bce6a144b7868b0e50b Mon Sep 17 00:00:00 2001 From: Juan Antonio Aldea Date: Sun, 2 Nov 2025 23:26:20 +0100 Subject: [PATCH 387/394] Use lists inits initialization instead of std::fill (#11532) --- esphome/components/remote_base/abbwelcome_protocol.h | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/esphome/components/remote_base/abbwelcome_protocol.h b/esphome/components/remote_base/abbwelcome_protocol.h index f2d0f5b547..b258bd920b 100644 --- a/esphome/components/remote_base/abbwelcome_protocol.h +++ b/esphome/components/remote_base/abbwelcome_protocol.h @@ -33,19 +33,13 @@ Message Format: class ABBWelcomeData { public: // Make default - ABBWelcomeData() { - std::fill(std::begin(this->data_), std::end(this->data_), 0); - this->data_[0] = 0x55; - this->data_[1] = 0xff; - } + ABBWelcomeData() : data_{0x55, 0xff} {} // Make from initializer_list - ABBWelcomeData(std::initializer_list data) { - std::fill(std::begin(this->data_), std::end(this->data_), 0); + ABBWelcomeData(std::initializer_list data) : data_{} { std::copy_n(data.begin(), std::min(data.size(), this->data_.size()), this->data_.begin()); } // Make from vector - ABBWelcomeData(const std::vector &data) { - std::fill(std::begin(this->data_), std::end(this->data_), 0); + ABBWelcomeData(const std::vector &data) : data_{} { std::copy_n(data.begin(), std::min(data.size(), this->data_.size()), this->data_.begin()); } // Default copy constructor From 8a8a80e1071a3f71938fdd278fca20a5eb5157a1 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Sun, 2 Nov 2025 23:44:52 +0100 Subject: [PATCH 388/394] [nrf52, zigbee] OnlyWith support list of components (#11533) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- esphome/config_validation.py | 31 ++++- tests/unit_tests/test_config_validation.py | 129 ++++++++++++++++++++- 2 files changed, 154 insertions(+), 6 deletions(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 359b257992..a3fd271a86 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Callable from contextlib import contextmanager, suppress from dataclasses import dataclass from datetime import datetime @@ -18,6 +19,7 @@ import logging from pathlib import Path import re from string import ascii_letters, digits +import typing import uuid as uuid_ import voluptuous as vol @@ -1763,16 +1765,37 @@ class SplitDefault(Optional): class OnlyWith(Optional): - """Set the default value only if the given component is loaded.""" + """Set the default value only if the given component(s) is/are loaded. - def __init__(self, key, component, default=None): + This validator allows configuration keys to have defaults that are only applied + when specific component(s) are loaded. Supports both single component names and + lists of components. + + Args: + key: Configuration key + component: Single component name (str) or list of component names. + For lists, ALL components must be loaded for the default to apply. + default: Default value to use when condition is met + + Example: + # Single component + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(MQTTComponent) + + # Multiple components (all must be loaded) + cv.OnlyWith(CONF_ZIGBEE_ID, ["zigbee", "nrf52"]): cv.use_id(Zigbee) + """ + + def __init__(self, key, component: str | list[str], default=None) -> None: super().__init__(key) self._component = component self._default = vol.default_factory(default) @property - def default(self): - if self._component in CORE.loaded_integrations: + def default(self) -> Callable[[], typing.Any] | vol.Undefined: + if isinstance(self._component, list): + if all(c in CORE.loaded_integrations for c in self._component): + return self._default + elif self._component in CORE.loaded_integrations: return self._default return vol.UNDEFINED diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index 2928c5c83a..104cdc2b7a 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -3,6 +3,7 @@ import string from hypothesis import example, given from hypothesis.strategies import builds, integers, ip_addresses, one_of, text import pytest +import voluptuous as vol from esphome import config_validation from esphome.components.esp32.const import ( @@ -301,8 +302,6 @@ def test_split_default(framework, platform, variant, full, idf, arduino, simple) ], ) def test_require_framework_version(framework, platform, message): - import voluptuous as vol - from esphome.const import ( KEY_CORE, KEY_FRAMEWORK_VERSION, @@ -377,3 +376,129 @@ def test_require_framework_version(framework, platform, message): config_validation.require_framework_version( extra_message="test 5", )("test") + + +def test_only_with_single_component_loaded() -> None: + """Test OnlyWith with single component when component is loaded.""" + CORE.loaded_integrations = {"mqtt"} + + schema = config_validation.Schema( + { + config_validation.OnlyWith("mqtt_id", "mqtt", default="test_mqtt"): str, + } + ) + + result = schema({}) + assert result.get("mqtt_id") == "test_mqtt" + + +def test_only_with_single_component_not_loaded() -> None: + """Test OnlyWith with single component when component is not loaded.""" + CORE.loaded_integrations = set() + + schema = config_validation.Schema( + { + config_validation.OnlyWith("mqtt_id", "mqtt", default="test_mqtt"): str, + } + ) + + result = schema({}) + assert "mqtt_id" not in result + + +def test_only_with_list_all_components_loaded() -> None: + """Test OnlyWith with list when all components are loaded.""" + CORE.loaded_integrations = {"zigbee", "nrf52"} + + schema = config_validation.Schema( + { + config_validation.OnlyWith( + "zigbee_id", ["zigbee", "nrf52"], default="test_zigbee" + ): str, + } + ) + + result = schema({}) + assert result.get("zigbee_id") == "test_zigbee" + + +def test_only_with_list_partial_components_loaded() -> None: + """Test OnlyWith with list when only some components are loaded.""" + CORE.loaded_integrations = {"zigbee"} # Only zigbee, not nrf52 + + schema = config_validation.Schema( + { + config_validation.OnlyWith( + "zigbee_id", ["zigbee", "nrf52"], default="test_zigbee" + ): str, + } + ) + + result = schema({}) + assert "zigbee_id" not in result + + +def test_only_with_list_no_components_loaded() -> None: + """Test OnlyWith with list when no components are loaded.""" + CORE.loaded_integrations = set() + + schema = config_validation.Schema( + { + config_validation.OnlyWith( + "zigbee_id", ["zigbee", "nrf52"], default="test_zigbee" + ): str, + } + ) + + result = schema({}) + assert "zigbee_id" not in result + + +def test_only_with_list_multiple_components() -> None: + """Test OnlyWith with list requiring three components.""" + CORE.loaded_integrations = {"comp1", "comp2", "comp3"} + + schema = config_validation.Schema( + { + config_validation.OnlyWith( + "test_id", ["comp1", "comp2", "comp3"], default="test_value" + ): str, + } + ) + + result = schema({}) + assert result.get("test_id") == "test_value" + + # Test with one missing + CORE.loaded_integrations = {"comp1", "comp2"} + result = schema({}) + assert "test_id" not in result + + +def test_only_with_empty_list() -> None: + """Test OnlyWith with empty list (edge case).""" + CORE.loaded_integrations = set() + + schema = config_validation.Schema( + { + config_validation.OnlyWith("test_id", [], default="test_value"): str, + } + ) + + # all([]) returns True, so default should be applied + result = schema({}) + assert result.get("test_id") == "test_value" + + +def test_only_with_user_value_overrides_default() -> None: + """Test OnlyWith respects user-provided values over defaults.""" + CORE.loaded_integrations = {"mqtt"} + + schema = config_validation.Schema( + { + config_validation.OnlyWith("mqtt_id", "mqtt", default="default_id"): str, + } + ) + + result = schema({"mqtt_id": "custom_id"}) + assert result.get("mqtt_id") == "custom_id" From 86402be9e31e7f983011797a1ce684b76c124441 Mon Sep 17 00:00:00 2001 From: Kjell Braden Date: Mon, 3 Nov 2025 00:02:13 +0100 Subject: [PATCH 389/394] actions: fix loop re-entry (#7972) Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Co-authored-by: J. Nick Koston Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- esphome/components/script/script.h | 38 ++++- esphome/core/base_automation.h | 80 +++++++---- .../fixtures/automation_wait_actions.yaml | 130 ++++++++++++++++++ .../test_action_concurrent_reentry.py | 1 - .../test_automation_wait_actions.py | 104 ++++++++++++++ 5 files changed, 321 insertions(+), 32 deletions(-) create mode 100644 tests/integration/fixtures/automation_wait_actions.yaml create mode 100644 tests/integration/test_automation_wait_actions.py diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 58fb67a3ea..870a623f32 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -2,6 +2,7 @@ #include #include +#include #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -264,10 +265,22 @@ template class IsRunningCondition : public Condition class ScriptWaitAction : public Action, public Component { public: ScriptWaitAction(C *script) : script_(script) {} + void setup() override { + // Start with loop disabled - only enable when there's work to do + this->disable_loop(); + } + void play_complex(Ts... x) override { this->num_running_++; // Check if we can continue immediately. @@ -275,7 +288,11 @@ template class ScriptWaitAction : public Action, this->play_next_(x...); return; } - this->var_ = std::make_tuple(x...); + + // Store parameters for later execution + this->param_queue_.emplace_front(x...); + // Enable loop now that we have work to do + this->enable_loop(); this->loop(); } @@ -286,15 +303,30 @@ template class ScriptWaitAction : public Action, if (this->script_->is_running()) return; - this->play_next_tuple_(this->var_); + while (!this->param_queue_.empty()) { + auto ¶ms = this->param_queue_.front(); + this->play_next_tuple_(params, typename gens::type()); + this->param_queue_.pop_front(); + } + // Queue is now empty - disable loop until next play_complex + this->disable_loop(); } void play(Ts... x) override { /* ignore - see play_complex */ } + void stop() override { + this->param_queue_.clear(); + this->disable_loop(); + } + protected: + template void play_next_tuple_(const std::tuple &tuple, seq /*unused*/) { + this->play_next_(std::get(tuple)...); + } + C *script_; - std::tuple var_{}; + std::forward_list> param_queue_; }; } // namespace script diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 1c60dd1c7a..e668a1782a 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -10,6 +10,7 @@ #include "esphome/core/helpers.h" #include +#include namespace esphome { @@ -268,32 +269,28 @@ template class WhileAction : public Action { void add_then(const std::initializer_list *> &actions) { this->then_.add_actions(actions); this->then_.add_action(new LambdaAction([this](Ts... x) { - if (this->num_running_ > 0 && this->condition_->check_tuple(this->var_)) { + if (this->num_running_ > 0 && this->condition_->check(x...)) { // play again - if (this->num_running_ > 0) { - this->then_.play_tuple(this->var_); - } + this->then_.play(x...); } else { // condition false, play next - this->play_next_tuple_(this->var_); + this->play_next_(x...); } })); } void play_complex(Ts... x) override { this->num_running_++; - // Store loop parameters - this->var_ = std::make_tuple(x...); // Initial condition check - if (!this->condition_->check_tuple(this->var_)) { + if (!this->condition_->check(x...)) { // If new condition check failed, stop loop if running this->then_.stop(); - this->play_next_tuple_(this->var_); + this->play_next_(x...); return; } if (this->num_running_ > 0) { - this->then_.play_tuple(this->var_); + this->then_.play(x...); } } @@ -305,7 +302,6 @@ template class WhileAction : public Action { protected: Condition *condition_; ActionList then_; - std::tuple var_{}; }; template class RepeatAction : public Action { @@ -317,7 +313,7 @@ template class RepeatAction : public Action { this->then_.add_action(new LambdaAction([this](uint32_t iteration, Ts... x) { iteration++; if (iteration >= this->count_.value(x...)) { - this->play_next_tuple_(this->var_); + this->play_next_(x...); } else { this->then_.play(iteration, x...); } @@ -326,11 +322,10 @@ template class RepeatAction : public Action { void play_complex(Ts... x) override { this->num_running_++; - this->var_ = std::make_tuple(x...); if (this->count_.value(x...) > 0) { this->then_.play(0, x...); } else { - this->play_next_tuple_(this->var_); + this->play_next_(x...); } } @@ -341,15 +336,26 @@ template class RepeatAction : public Action { protected: ActionList then_; - std::tuple var_; }; +/** Wait until a condition is true to continue execution. + * + * Uses queue-based storage to safely handle concurrent executions. + * While concurrent execution from the same trigger is uncommon, it's possible + * (e.g., rapid button presses, high-frequency sensor updates), so we use + * queue-based storage for correctness. + */ template class WaitUntilAction : public Action, public Component { public: WaitUntilAction(Condition *condition) : condition_(condition) {} TEMPLATABLE_VALUE(uint32_t, timeout_value) + void setup() override { + // Start with loop disabled - only enable when there's work to do + this->disable_loop(); + } + void play_complex(Ts... x) override { this->num_running_++; // Check if we can continue immediately. @@ -359,13 +365,14 @@ template class WaitUntilAction : public Action, public Co } return; } - this->var_ = std::make_tuple(x...); - if (this->timeout_value_.has_value()) { - auto f = std::bind(&WaitUntilAction::play_next_, this, x...); - this->set_timeout("timeout", this->timeout_value_.value(x...), f); - } + // Store for later processing + auto now = millis(); + auto timeout = this->timeout_value_.optional_value(x...); + this->var_queue_.emplace_front(now, timeout, std::make_tuple(x...)); + // Enable loop now that we have work to do + this->enable_loop(); this->loop(); } @@ -373,13 +380,32 @@ template class WaitUntilAction : public Action, public Co if (this->num_running_ == 0) return; - if (!this->condition_->check_tuple(this->var_)) { - return; + auto now = millis(); + + this->var_queue_.remove_if([&](auto &queued) { + auto start = std::get(queued); + auto timeout = std::get>(queued); + auto &var = std::get>(queued); + + auto expired = timeout && (now - start) >= *timeout; + + if (!expired && !this->condition_->check_tuple(var)) { + return false; + } + + this->play_next_tuple_(var); + return true; + }); + + // If queue is now empty, disable loop until next play_complex + if (this->var_queue_.empty()) { + this->disable_loop(); } + } - this->cancel_timeout("timeout"); - - this->play_next_tuple_(this->var_); + void stop() override { + this->var_queue_.clear(); + this->disable_loop(); } float get_setup_priority() const override { return setup_priority::DATA; } @@ -387,11 +413,9 @@ template class WaitUntilAction : public Action, public Co void play(Ts... x) override { /* ignore - see play_complex */ } - void stop() override { this->cancel_timeout("timeout"); } - protected: Condition *condition_; - std::tuple var_{}; + std::forward_list, std::tuple>> var_queue_{}; }; template class UpdateComponentAction : public Action { diff --git a/tests/integration/fixtures/automation_wait_actions.yaml b/tests/integration/fixtures/automation_wait_actions.yaml new file mode 100644 index 0000000000..65a61be14f --- /dev/null +++ b/tests/integration/fixtures/automation_wait_actions.yaml @@ -0,0 +1,130 @@ +esphome: + name: test-automation-wait-actions + +host: + +api: + actions: + # Test 1: Trigger wait_until automation 5 times rapidly + - action: test_wait_until + then: + - logger.log: "=== TEST 1: Triggering wait_until automation 5 times ===" + # Publish 5 different values to trigger the on_value automation 5 times + - sensor.template.publish: + id: wait_until_sensor + state: 1 + - sensor.template.publish: + id: wait_until_sensor + state: 2 + - sensor.template.publish: + id: wait_until_sensor + state: 3 + - sensor.template.publish: + id: wait_until_sensor + state: 4 + - sensor.template.publish: + id: wait_until_sensor + state: 5 + # Wait then satisfy the condition so all 5 waiting actions complete + - delay: 100ms + - globals.set: + id: test_flag + value: 'true' + + # Test 2: Trigger script.wait automation 5 times rapidly + - action: test_script_wait + then: + - logger.log: "=== TEST 2: Triggering script.wait automation 5 times ===" + # Start a long-running script + - script.execute: blocking_script + # Publish 5 different values to trigger the on_value automation 5 times + - sensor.template.publish: + id: script_wait_sensor + state: 1 + - sensor.template.publish: + id: script_wait_sensor + state: 2 + - sensor.template.publish: + id: script_wait_sensor + state: 3 + - sensor.template.publish: + id: script_wait_sensor + state: 4 + - sensor.template.publish: + id: script_wait_sensor + state: 5 + + # Test 3: Trigger wait_until timeout automation 5 times rapidly + - action: test_wait_timeout + then: + - logger.log: "=== TEST 3: Triggering timeout automation 5 times ===" + # Publish 5 different values (condition will never be true, all will timeout) + - sensor.template.publish: + id: timeout_sensor + state: 1 + - sensor.template.publish: + id: timeout_sensor + state: 2 + - sensor.template.publish: + id: timeout_sensor + state: 3 + - sensor.template.publish: + id: timeout_sensor + state: 4 + - sensor.template.publish: + id: timeout_sensor + state: 5 + +logger: + level: DEBUG + +globals: + - id: test_flag + type: bool + restore_value: false + initial_value: 'false' + + - id: timeout_flag + type: bool + restore_value: false + initial_value: 'false' + +# Sensors with wait_until/script.wait in their on_value automations +sensor: + # Test 1: on_value automation with wait_until + - platform: template + id: wait_until_sensor + on_value: + # This wait_until will be hit 5 times before any complete + - wait_until: + condition: + lambda: return id(test_flag); + - logger.log: "wait_until automation completed" + + # Test 2: on_value automation with script.wait + - platform: template + id: script_wait_sensor + on_value: + # This script.wait will be hit 5 times before any complete + - script.wait: blocking_script + - logger.log: "script.wait automation completed" + + # Test 3: on_value automation with wait_until timeout + - platform: template + id: timeout_sensor + on_value: + # This wait_until will be hit 5 times, all will timeout + - wait_until: + condition: + lambda: return id(timeout_flag); + timeout: 200ms + - logger.log: "timeout automation completed" + +script: + # Blocking script for script.wait test + - id: blocking_script + mode: single + then: + - logger.log: "Blocking script: START" + - delay: 200ms + - logger.log: "Blocking script: END" diff --git a/tests/integration/test_action_concurrent_reentry.py b/tests/integration/test_action_concurrent_reentry.py index ba67e4c798..aa5801ca2b 100644 --- a/tests/integration/test_action_concurrent_reentry.py +++ b/tests/integration/test_action_concurrent_reentry.py @@ -11,7 +11,6 @@ import pytest from .types import APIClientConnectedFactory, RunCompiledFunction -@pytest.mark.xfail(reason="https://github.com/esphome/issues/issues/6534") @pytest.mark.asyncio async def test_action_concurrent_reentry( yaml_config: str, diff --git a/tests/integration/test_automation_wait_actions.py b/tests/integration/test_automation_wait_actions.py new file mode 100644 index 0000000000..adcb8ba487 --- /dev/null +++ b/tests/integration/test_automation_wait_actions.py @@ -0,0 +1,104 @@ +"""Test concurrent execution of wait_until and script.wait in direct automation actions.""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_automation_wait_actions( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """ + Test that wait_until and script.wait correctly handle concurrent executions + when automation actions (not scripts) are triggered multiple times rapidly. + + This tests sensor.on_value automations being triggered 5 times before any complete. + """ + loop = asyncio.get_running_loop() + + # Track completion counts + test_results = { + "wait_until": 0, + "script_wait": 0, + "wait_until_timeout": 0, + } + + # Patterns for log messages + wait_until_complete = re.compile(r"wait_until automation completed") + script_wait_complete = re.compile(r"script\.wait automation completed") + timeout_complete = re.compile(r"timeout automation completed") + + # Test completion futures + test1_complete = loop.create_future() + test2_complete = loop.create_future() + test3_complete = loop.create_future() + + def check_output(line: str) -> None: + """Check log output for completion messages.""" + # Test 1: wait_until concurrent execution + if wait_until_complete.search(line): + test_results["wait_until"] += 1 + if test_results["wait_until"] == 5 and not test1_complete.done(): + test1_complete.set_result(True) + + # Test 2: script.wait concurrent execution + if script_wait_complete.search(line): + test_results["script_wait"] += 1 + if test_results["script_wait"] == 5 and not test2_complete.done(): + test2_complete.set_result(True) + + # Test 3: wait_until with timeout + if timeout_complete.search(line): + test_results["wait_until_timeout"] += 1 + if test_results["wait_until_timeout"] == 5 and not test3_complete.done(): + test3_complete.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Get services + _, services = await client.list_entities_services() + + # Test 1: wait_until in automation - trigger 5 times rapidly + test_service = next((s for s in services if s.name == "test_wait_until"), None) + assert test_service is not None, "test_wait_until service not found" + client.execute_service(test_service, {}) + await asyncio.wait_for(test1_complete, timeout=3.0) + + # Verify Test 1: All 5 triggers should complete + assert test_results["wait_until"] == 5, ( + f"Test 1: Expected 5 wait_until completions, got {test_results['wait_until']}" + ) + + # Test 2: script.wait in automation - trigger 5 times rapidly + test_service = next((s for s in services if s.name == "test_script_wait"), None) + assert test_service is not None, "test_script_wait service not found" + client.execute_service(test_service, {}) + await asyncio.wait_for(test2_complete, timeout=3.0) + + # Verify Test 2: All 5 triggers should complete + assert test_results["script_wait"] == 5, ( + f"Test 2: Expected 5 script.wait completions, got {test_results['script_wait']}" + ) + + # Test 3: wait_until with timeout in automation - trigger 5 times rapidly + test_service = next( + (s for s in services if s.name == "test_wait_timeout"), None + ) + assert test_service is not None, "test_wait_timeout service not found" + client.execute_service(test_service, {}) + await asyncio.wait_for(test3_complete, timeout=3.0) + + # Verify Test 3: All 5 triggers should timeout and complete + assert test_results["wait_until_timeout"] == 5, ( + f"Test 3: Expected 5 timeout completions, got {test_results['wait_until_timeout']}" + ) From 035a510aba8e0c03aae0bbe8ae919b7e2df04444 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 17:11:13 -0600 Subject: [PATCH 390/394] fix conflict --- esphome/core/base_automation.h | 59 +++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index efe9aa1c47..541911f22a 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -406,12 +406,24 @@ template void RepeatLoopContinuation::play(uint32_t itera } } +/** Wait until a condition is true to continue execution. + * + * Uses queue-based storage to safely handle concurrent executions. + * While concurrent execution from the same trigger is uncommon, it's possible + * (e.g., rapid button presses, high-frequency sensor updates), so we use + * queue-based storage for correctness. + */ template class WaitUntilAction : public Action, public Component { public: WaitUntilAction(Condition *condition) : condition_(condition) {} TEMPLATABLE_VALUE(uint32_t, timeout_value) + void setup() override { + // Start with loop disabled - only enable when there's work to do + this->disable_loop(); + } + void play_complex(Ts... x) override { this->num_running_++; // Check if we can continue immediately. @@ -421,16 +433,14 @@ template class WaitUntilAction : public Action, public Co } return; } - this->var_ = std::make_tuple(x...); - if (this->timeout_value_.has_value()) { - // Lambda captures only 'this' to reference stored var_ - // vs std::bind which duplicates storage of x... (already in var_) - // This eliminates ~100-200 bytes of std::bind template instantiation code - auto f = [this]() { this->play_next_tuple_(this->var_); }; - this->set_timeout("timeout", this->timeout_value_.value(x...), f); - } + // Store for later processing + auto now = millis(); + auto timeout = this->timeout_value_.optional_value(x...); + this->var_queue_.emplace_front(now, timeout, std::make_tuple(x...)); + // Enable loop now that we have work to do + this->enable_loop(); this->loop(); } @@ -438,13 +448,32 @@ template class WaitUntilAction : public Action, public Co if (this->num_running_ == 0) return; - if (!this->condition_->check_tuple(this->var_)) { - return; + auto now = millis(); + + this->var_queue_.remove_if([&](auto &queued) { + auto start = std::get(queued); + auto timeout = std::get>(queued); + auto &var = std::get>(queued); + + auto expired = timeout && (now - start) >= *timeout; + + if (!expired && !this->condition_->check_tuple(var)) { + return false; + } + + this->play_next_tuple_(var); + return true; + }); + + // If queue is now empty, disable loop until next play_complex + if (this->var_queue_.empty()) { + this->disable_loop(); } + } - this->cancel_timeout("timeout"); - - this->play_next_tuple_(this->var_); + void stop() override { + this->var_queue_.clear(); + this->disable_loop(); } float get_setup_priority() const override { return setup_priority::DATA; } @@ -452,11 +481,9 @@ template class WaitUntilAction : public Action, public Co void play(Ts... x) override { /* ignore - see play_complex */ } - void stop() override { this->cancel_timeout("timeout"); } - protected: Condition *condition_; - std::tuple var_{}; + std::forward_list, std::tuple>> var_queue_{}; }; template class UpdateComponentAction : public Action { From 2f35a94d282f6358ed929062268fefc9774f61ec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 17:13:56 -0600 Subject: [PATCH 391/394] revert --- esphome/core/base_automation.h | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 541911f22a..a31c501060 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -172,22 +172,9 @@ template class DelayAction : public Action, public Compon TEMPLATABLE_VALUE(uint32_t, delay) void play_complex(Ts... x) override { + auto f = std::bind(&DelayAction::play_next_, this, x...); this->num_running_++; - // Store parameters in shared_ptr for this timer instance - // This avoids std::bind bloat while supporting parallel script mode - // shared_ptr is used (vs unique_ptr) because std::function requires copyability - auto params = std::make_shared>(x...); - - // Lambda captures only 'this' and the shared_ptr (8-16 bytes total) - // vs std::bind which captures 'this' + copies of all x... + bind overhead - // This eliminates ~200-300 bytes of std::bind template instantiation code - auto f = [this, params]() { - if (this->num_running_ > 0) { - std::apply([this](auto &&...args) { this->play_next_(args...); }, *params); - } - }; - // If num_running_ > 1, we have multiple instances running in parallel // In single/restart/queued modes, only one instance runs at a time // Parallel mode uses skip_cancel=true to allow multiple delays to coexist From 21a343701df69635e4b1a7a4891b62ad903c419f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 17:21:03 -0600 Subject: [PATCH 392/394] cover --- tests/components/api/common-base.yaml | 96 +++++++ .../fixtures/continuation_actions.yaml | 174 +++++++++++++ .../integration/test_continuation_actions.py | 235 ++++++++++++++++++ 3 files changed, 505 insertions(+) create mode 100644 tests/integration/fixtures/continuation_actions.yaml create mode 100644 tests/integration/test_continuation_actions.py diff --git a/tests/components/api/common-base.yaml b/tests/components/api/common-base.yaml index 6483d5a997..c90fa4dfef 100644 --- a/tests/components/api/common-base.yaml +++ b/tests/components/api/common-base.yaml @@ -87,3 +87,99 @@ api: - float_arr.size() - string_arr[0].c_str() - string_arr.size() + # Test ContinuationAction (IfAction with then/else branches) + - action: test_if_action + variables: + condition: bool + value: int + then: + - if: + condition: + lambda: 'return condition;' + then: + - logger.log: + format: "Condition true, value: %d" + args: ['value'] + else: + - logger.log: + format: "Condition false, value: %d" + args: ['value'] + - logger.log: "After if/else" + # Test nested IfAction (multiple ContinuationAction instances) + - action: test_nested_if + variables: + outer: bool + inner: bool + then: + - if: + condition: + lambda: 'return outer;' + then: + - if: + condition: + lambda: 'return inner;' + then: + - logger.log: "Both true" + else: + - logger.log: "Outer true, inner false" + else: + - logger.log: "Outer false" + - logger.log: "After nested if" + # Test WhileLoopContinuation (WhileAction) + - action: test_while_action + variables: + max_count: int + then: + - lambda: 'id(api_continuation_test_counter) = 0;' + - while: + condition: + lambda: 'return id(api_continuation_test_counter) < max_count;' + then: + - logger.log: + format: "While loop iteration: %d" + args: ['id(api_continuation_test_counter)'] + - lambda: 'id(api_continuation_test_counter)++;' + - logger.log: "After while loop" + # Test RepeatLoopContinuation (RepeatAction) + - action: test_repeat_action + variables: + count: int + then: + - repeat: + count: !lambda 'return count;' + then: + - logger.log: + format: "Repeat iteration: %d" + args: ['iteration'] + - logger.log: "After repeat" + # Test combined continuations (if + while + repeat) + - action: test_combined_continuations + variables: + do_loop: bool + loop_count: int + then: + - if: + condition: + lambda: 'return do_loop;' + then: + - repeat: + count: !lambda 'return loop_count;' + then: + - lambda: 'id(api_continuation_test_counter) = iteration;' + - while: + condition: + lambda: 'return id(api_continuation_test_counter) > 0;' + then: + - logger.log: + format: "Combined: repeat=%d, while=%d" + args: ['iteration', 'id(api_continuation_test_counter)'] + - lambda: 'id(api_continuation_test_counter)--;' + else: + - logger.log: "Skipped loops" + - logger.log: "After combined test" + +globals: + - id: api_continuation_test_counter + type: int + restore_value: false + initial_value: '0' diff --git a/tests/integration/fixtures/continuation_actions.yaml b/tests/integration/fixtures/continuation_actions.yaml new file mode 100644 index 0000000000..bdfe149cb7 --- /dev/null +++ b/tests/integration/fixtures/continuation_actions.yaml @@ -0,0 +1,174 @@ +esphome: + name: test-continuation-actions + +host: + +api: + actions: + # Test 1: IfAction with ContinuationAction (then/else branches) + - action: test_if_action + variables: + condition: bool + value: int + then: + - logger.log: + format: "Test if: condition=%s, value=%d" + args: ['YESNO(condition)', 'value'] + - if: + condition: + lambda: 'return condition;' + then: + - logger.log: + format: "if-then executed: value=%d" + args: ['value'] + else: + - logger.log: + format: "if-else executed: value=%d" + args: ['value'] + - logger.log: "if completed" + + # Test 2: Nested IfAction (multiple ContinuationAction instances) + - action: test_nested_if + variables: + outer: bool + inner: bool + then: + - logger.log: + format: "Test nested if: outer=%s, inner=%s" + args: ['YESNO(outer)', 'YESNO(inner)'] + - if: + condition: + lambda: 'return outer;' + then: + - if: + condition: + lambda: 'return inner;' + then: + - logger.log: "nested-both-true" + else: + - logger.log: "nested-outer-true-inner-false" + else: + - logger.log: "nested-outer-false" + - logger.log: "nested if completed" + + # Test 3: WhileAction with WhileLoopContinuation + - action: test_while_action + variables: + max_count: int + then: + - logger.log: + format: "Test while: max_count=%d" + args: ['max_count'] + - globals.set: + id: continuation_test_counter + value: !lambda 'return 0;' + - while: + condition: + lambda: 'return id(continuation_test_counter) < max_count;' + then: + - logger.log: + format: "while-iteration-%d" + args: ['id(continuation_test_counter)'] + - globals.set: + id: continuation_test_counter + value: !lambda 'return id(continuation_test_counter) + 1;' + - logger.log: "while completed" + + # Test 4: RepeatAction with RepeatLoopContinuation + - action: test_repeat_action + variables: + count: int + then: + - logger.log: + format: "Test repeat: count=%d" + args: ['count'] + - repeat: + count: !lambda 'return count;' + then: + - logger.log: + format: "repeat-iteration-%d" + args: ['iteration'] + - logger.log: "repeat completed" + + # Test 5: Combined continuations (if + while + repeat) + - action: test_combined + variables: + do_loop: bool + loop_count: int + then: + - logger.log: + format: "Test combined: do_loop=%s, loop_count=%d" + args: ['YESNO(do_loop)', 'loop_count'] + - if: + condition: + lambda: 'return do_loop;' + then: + - repeat: + count: !lambda 'return loop_count;' + then: + - globals.set: + id: continuation_test_counter + value: !lambda 'return iteration;' + - while: + condition: + lambda: 'return id(continuation_test_counter) > 0;' + then: + - logger.log: + format: "combined-repeat%d-while%d" + args: ['iteration', 'id(continuation_test_counter)'] + - globals.set: + id: continuation_test_counter + value: !lambda 'return id(continuation_test_counter) - 1;' + else: + - logger.log: "combined-skipped" + - logger.log: "combined completed" + + # Test 6: Rapid triggers to verify memory efficiency + - action: test_rapid_if + then: + - logger.log: "=== Rapid if test start ===" + - sensor.template.publish: + id: rapid_sensor + state: 1 + - sensor.template.publish: + id: rapid_sensor + state: 2 + - sensor.template.publish: + id: rapid_sensor + state: 3 + - sensor.template.publish: + id: rapid_sensor + state: 4 + - sensor.template.publish: + id: rapid_sensor + state: 5 + - logger.log: "=== Rapid if test published 5 values ===" + +logger: + level: DEBUG + +globals: + - id: continuation_test_counter + type: int + restore_value: false + initial_value: '0' + +# Sensor to test rapid automation triggers with if/else (ContinuationAction) +sensor: + - platform: template + id: rapid_sensor + on_value: + - if: + condition: + lambda: 'return x > 2;' + then: + - logger.log: + format: "rapid-if-then: value=%d" + args: ['(int)x'] + else: + - logger.log: + format: "rapid-if-else: value=%d" + args: ['(int)x'] + - logger.log: + format: "rapid-if-completed: value=%d" + args: ['(int)x'] diff --git a/tests/integration/test_continuation_actions.py b/tests/integration/test_continuation_actions.py new file mode 100644 index 0000000000..1069ee7581 --- /dev/null +++ b/tests/integration/test_continuation_actions.py @@ -0,0 +1,235 @@ +"""Test continuation actions (ContinuationAction, WhileLoopContinuation, RepeatLoopContinuation).""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_continuation_actions( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """ + Test that continuation actions work correctly for if/while/repeat. + + These continuation classes replace LambdaAction with simple parent pointers, + saving 32-36 bytes per instance and eliminating std::function overhead. + """ + loop = asyncio.get_running_loop() + + # Track test completions + test_results = { + "if_then": False, + "if_else": False, + "if_complete": False, + "nested_both_true": False, + "nested_outer_true_inner_false": False, + "nested_outer_false": False, + "nested_complete": False, + "while_iterations": 0, + "while_complete": False, + "repeat_iterations": 0, + "repeat_complete": False, + "combined_iterations": 0, + "combined_complete": False, + "rapid_then": 0, + "rapid_else": 0, + "rapid_complete": 0, + } + + # Patterns for log messages + if_then_pattern = re.compile(r"if-then executed: value=(\d+)") + if_else_pattern = re.compile(r"if-else executed: value=(\d+)") + if_complete_pattern = re.compile(r"if completed") + nested_both_true_pattern = re.compile(r"nested-both-true") + nested_outer_true_inner_false_pattern = re.compile(r"nested-outer-true-inner-false") + nested_outer_false_pattern = re.compile(r"nested-outer-false") + nested_complete_pattern = re.compile(r"nested if completed") + while_iteration_pattern = re.compile(r"while-iteration-(\d+)") + while_complete_pattern = re.compile(r"while completed") + repeat_iteration_pattern = re.compile(r"repeat-iteration-(\d+)") + repeat_complete_pattern = re.compile(r"repeat completed") + combined_pattern = re.compile(r"combined-repeat(\d+)-while(\d+)") + combined_complete_pattern = re.compile(r"combined completed") + rapid_then_pattern = re.compile(r"rapid-if-then: value=(\d+)") + rapid_else_pattern = re.compile(r"rapid-if-else: value=(\d+)") + rapid_complete_pattern = re.compile(r"rapid-if-completed: value=(\d+)") + + # Test completion futures + test1_complete = loop.create_future() # if action + test2_complete = loop.create_future() # nested if + test3_complete = loop.create_future() # while + test4_complete = loop.create_future() # repeat + test5_complete = loop.create_future() # combined + test6_complete = loop.create_future() # rapid + + def check_output(line: str) -> None: + """Check log output for test messages.""" + # Test 1: IfAction + if if_then_pattern.search(line): + test_results["if_then"] = True + if if_else_pattern.search(line): + test_results["if_else"] = True + if if_complete_pattern.search(line): + test_results["if_complete"] = True + if not test1_complete.done(): + test1_complete.set_result(True) + + # Test 2: Nested IfAction + if nested_both_true_pattern.search(line): + test_results["nested_both_true"] = True + if nested_outer_true_inner_false_pattern.search(line): + test_results["nested_outer_true_inner_false"] = True + if nested_outer_false_pattern.search(line): + test_results["nested_outer_false"] = True + if nested_complete_pattern.search(line): + test_results["nested_complete"] = True + if not test2_complete.done(): + test2_complete.set_result(True) + + # Test 3: WhileAction + if match := while_iteration_pattern.search(line): + test_results["while_iterations"] = max( + test_results["while_iterations"], int(match.group(1)) + 1 + ) + if while_complete_pattern.search(line): + test_results["while_complete"] = True + if not test3_complete.done(): + test3_complete.set_result(True) + + # Test 4: RepeatAction + if match := repeat_iteration_pattern.search(line): + test_results["repeat_iterations"] = max( + test_results["repeat_iterations"], int(match.group(1)) + 1 + ) + if repeat_complete_pattern.search(line): + test_results["repeat_complete"] = True + if not test4_complete.done(): + test4_complete.set_result(True) + + # Test 5: Combined + if combined_pattern.search(line): + test_results["combined_iterations"] += 1 + if combined_complete_pattern.search(line): + test_results["combined_complete"] = True + if not test5_complete.done(): + test5_complete.set_result(True) + + # Test 6: Rapid triggers + if rapid_then_pattern.search(line): + test_results["rapid_then"] += 1 + if rapid_else_pattern.search(line): + test_results["rapid_else"] += 1 + if rapid_complete_pattern.search(line): + test_results["rapid_complete"] += 1 + if test_results["rapid_complete"] == 5 and not test6_complete.done(): + test6_complete.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Get services + _, services = await client.list_entities_services() + + # Test 1: IfAction with then branch + test_service = next((s for s in services if s.name == "test_if_action"), None) + assert test_service is not None, "test_if_action service not found" + client.execute_service(test_service, {"condition": True, "value": 42}) + await asyncio.wait_for(test1_complete, timeout=2.0) + assert test_results["if_then"], "IfAction then branch not executed" + assert test_results["if_complete"], "IfAction did not complete" + + # Test 1b: IfAction with else branch + test1_complete = loop.create_future() + test_results["if_complete"] = False + client.execute_service(test_service, {"condition": False, "value": 99}) + await asyncio.wait_for(test1_complete, timeout=2.0) + assert test_results["if_else"], "IfAction else branch not executed" + assert test_results["if_complete"], "IfAction did not complete" + + # Test 2: Nested IfAction - test all branches + test_service = next((s for s in services if s.name == "test_nested_if"), None) + assert test_service is not None, "test_nested_if service not found" + + # Both true + client.execute_service(test_service, {"outer": True, "inner": True}) + await asyncio.wait_for(test2_complete, timeout=2.0) + assert test_results["nested_both_true"], "Nested both true not executed" + + # Outer true, inner false + test2_complete = loop.create_future() + test_results["nested_complete"] = False + client.execute_service(test_service, {"outer": True, "inner": False}) + await asyncio.wait_for(test2_complete, timeout=2.0) + assert test_results["nested_outer_true_inner_false"], ( + "Nested outer true inner false not executed" + ) + + # Outer false + test2_complete = loop.create_future() + test_results["nested_complete"] = False + client.execute_service(test_service, {"outer": False, "inner": True}) + await asyncio.wait_for(test2_complete, timeout=2.0) + assert test_results["nested_outer_false"], "Nested outer false not executed" + + # Test 3: WhileAction + test_service = next( + (s for s in services if s.name == "test_while_action"), None + ) + assert test_service is not None, "test_while_action service not found" + client.execute_service(test_service, {"max_count": 3}) + await asyncio.wait_for(test3_complete, timeout=2.0) + assert test_results["while_iterations"] == 3, ( + f"WhileAction expected 3 iterations, got {test_results['while_iterations']}" + ) + assert test_results["while_complete"], "WhileAction did not complete" + + # Test 4: RepeatAction + test_service = next( + (s for s in services if s.name == "test_repeat_action"), None + ) + assert test_service is not None, "test_repeat_action service not found" + client.execute_service(test_service, {"count": 5}) + await asyncio.wait_for(test4_complete, timeout=2.0) + assert test_results["repeat_iterations"] == 5, ( + f"RepeatAction expected 5 iterations, got {test_results['repeat_iterations']}" + ) + assert test_results["repeat_complete"], "RepeatAction did not complete" + + # Test 5: Combined (if + repeat + while) + test_service = next((s for s in services if s.name == "test_combined"), None) + assert test_service is not None, "test_combined service not found" + client.execute_service(test_service, {"do_loop": True, "loop_count": 2}) + await asyncio.wait_for(test5_complete, timeout=2.0) + # Should execute: repeat 2 times, each iteration does while from iteration down to 0 + # iteration 0: while 0 times = 0 + # iteration 1: while 1 time = 1 + # Total: 1 combined log + assert test_results["combined_iterations"] >= 1, ( + f"Combined expected >=1 iterations, got {test_results['combined_iterations']}" + ) + assert test_results["combined_complete"], "Combined did not complete" + + # Test 6: Rapid triggers (tests memory efficiency of ContinuationAction) + test_service = next((s for s in services if s.name == "test_rapid_if"), None) + assert test_service is not None, "test_rapid_if service not found" + client.execute_service(test_service, {}) + await asyncio.wait_for(test6_complete, timeout=2.0) + # Values 1, 2 should hit else (<=2), values 3, 4, 5 should hit then (>2) + assert test_results["rapid_else"] == 2, ( + f"Rapid test expected 2 else, got {test_results['rapid_else']}" + ) + assert test_results["rapid_then"] == 3, ( + f"Rapid test expected 3 then, got {test_results['rapid_then']}" + ) + assert test_results["rapid_complete"] == 5, ( + f"Rapid test expected 5 completions, got {test_results['rapid_complete']}" + ) From 47cc2403681cd59bbda0e34f19876a49ec778282 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 17:23:37 -0600 Subject: [PATCH 393/394] Add action continuation tests new baseline ahead of https://github.com/esphome/esphome/pull/11650 --- tests/components/api/common-base.yaml | 96 +++++++ .../fixtures/continuation_actions.yaml | 174 +++++++++++++ .../integration/test_continuation_actions.py | 235 ++++++++++++++++++ 3 files changed, 505 insertions(+) create mode 100644 tests/integration/fixtures/continuation_actions.yaml create mode 100644 tests/integration/test_continuation_actions.py diff --git a/tests/components/api/common-base.yaml b/tests/components/api/common-base.yaml index 6483d5a997..c90fa4dfef 100644 --- a/tests/components/api/common-base.yaml +++ b/tests/components/api/common-base.yaml @@ -87,3 +87,99 @@ api: - float_arr.size() - string_arr[0].c_str() - string_arr.size() + # Test ContinuationAction (IfAction with then/else branches) + - action: test_if_action + variables: + condition: bool + value: int + then: + - if: + condition: + lambda: 'return condition;' + then: + - logger.log: + format: "Condition true, value: %d" + args: ['value'] + else: + - logger.log: + format: "Condition false, value: %d" + args: ['value'] + - logger.log: "After if/else" + # Test nested IfAction (multiple ContinuationAction instances) + - action: test_nested_if + variables: + outer: bool + inner: bool + then: + - if: + condition: + lambda: 'return outer;' + then: + - if: + condition: + lambda: 'return inner;' + then: + - logger.log: "Both true" + else: + - logger.log: "Outer true, inner false" + else: + - logger.log: "Outer false" + - logger.log: "After nested if" + # Test WhileLoopContinuation (WhileAction) + - action: test_while_action + variables: + max_count: int + then: + - lambda: 'id(api_continuation_test_counter) = 0;' + - while: + condition: + lambda: 'return id(api_continuation_test_counter) < max_count;' + then: + - logger.log: + format: "While loop iteration: %d" + args: ['id(api_continuation_test_counter)'] + - lambda: 'id(api_continuation_test_counter)++;' + - logger.log: "After while loop" + # Test RepeatLoopContinuation (RepeatAction) + - action: test_repeat_action + variables: + count: int + then: + - repeat: + count: !lambda 'return count;' + then: + - logger.log: + format: "Repeat iteration: %d" + args: ['iteration'] + - logger.log: "After repeat" + # Test combined continuations (if + while + repeat) + - action: test_combined_continuations + variables: + do_loop: bool + loop_count: int + then: + - if: + condition: + lambda: 'return do_loop;' + then: + - repeat: + count: !lambda 'return loop_count;' + then: + - lambda: 'id(api_continuation_test_counter) = iteration;' + - while: + condition: + lambda: 'return id(api_continuation_test_counter) > 0;' + then: + - logger.log: + format: "Combined: repeat=%d, while=%d" + args: ['iteration', 'id(api_continuation_test_counter)'] + - lambda: 'id(api_continuation_test_counter)--;' + else: + - logger.log: "Skipped loops" + - logger.log: "After combined test" + +globals: + - id: api_continuation_test_counter + type: int + restore_value: false + initial_value: '0' diff --git a/tests/integration/fixtures/continuation_actions.yaml b/tests/integration/fixtures/continuation_actions.yaml new file mode 100644 index 0000000000..bdfe149cb7 --- /dev/null +++ b/tests/integration/fixtures/continuation_actions.yaml @@ -0,0 +1,174 @@ +esphome: + name: test-continuation-actions + +host: + +api: + actions: + # Test 1: IfAction with ContinuationAction (then/else branches) + - action: test_if_action + variables: + condition: bool + value: int + then: + - logger.log: + format: "Test if: condition=%s, value=%d" + args: ['YESNO(condition)', 'value'] + - if: + condition: + lambda: 'return condition;' + then: + - logger.log: + format: "if-then executed: value=%d" + args: ['value'] + else: + - logger.log: + format: "if-else executed: value=%d" + args: ['value'] + - logger.log: "if completed" + + # Test 2: Nested IfAction (multiple ContinuationAction instances) + - action: test_nested_if + variables: + outer: bool + inner: bool + then: + - logger.log: + format: "Test nested if: outer=%s, inner=%s" + args: ['YESNO(outer)', 'YESNO(inner)'] + - if: + condition: + lambda: 'return outer;' + then: + - if: + condition: + lambda: 'return inner;' + then: + - logger.log: "nested-both-true" + else: + - logger.log: "nested-outer-true-inner-false" + else: + - logger.log: "nested-outer-false" + - logger.log: "nested if completed" + + # Test 3: WhileAction with WhileLoopContinuation + - action: test_while_action + variables: + max_count: int + then: + - logger.log: + format: "Test while: max_count=%d" + args: ['max_count'] + - globals.set: + id: continuation_test_counter + value: !lambda 'return 0;' + - while: + condition: + lambda: 'return id(continuation_test_counter) < max_count;' + then: + - logger.log: + format: "while-iteration-%d" + args: ['id(continuation_test_counter)'] + - globals.set: + id: continuation_test_counter + value: !lambda 'return id(continuation_test_counter) + 1;' + - logger.log: "while completed" + + # Test 4: RepeatAction with RepeatLoopContinuation + - action: test_repeat_action + variables: + count: int + then: + - logger.log: + format: "Test repeat: count=%d" + args: ['count'] + - repeat: + count: !lambda 'return count;' + then: + - logger.log: + format: "repeat-iteration-%d" + args: ['iteration'] + - logger.log: "repeat completed" + + # Test 5: Combined continuations (if + while + repeat) + - action: test_combined + variables: + do_loop: bool + loop_count: int + then: + - logger.log: + format: "Test combined: do_loop=%s, loop_count=%d" + args: ['YESNO(do_loop)', 'loop_count'] + - if: + condition: + lambda: 'return do_loop;' + then: + - repeat: + count: !lambda 'return loop_count;' + then: + - globals.set: + id: continuation_test_counter + value: !lambda 'return iteration;' + - while: + condition: + lambda: 'return id(continuation_test_counter) > 0;' + then: + - logger.log: + format: "combined-repeat%d-while%d" + args: ['iteration', 'id(continuation_test_counter)'] + - globals.set: + id: continuation_test_counter + value: !lambda 'return id(continuation_test_counter) - 1;' + else: + - logger.log: "combined-skipped" + - logger.log: "combined completed" + + # Test 6: Rapid triggers to verify memory efficiency + - action: test_rapid_if + then: + - logger.log: "=== Rapid if test start ===" + - sensor.template.publish: + id: rapid_sensor + state: 1 + - sensor.template.publish: + id: rapid_sensor + state: 2 + - sensor.template.publish: + id: rapid_sensor + state: 3 + - sensor.template.publish: + id: rapid_sensor + state: 4 + - sensor.template.publish: + id: rapid_sensor + state: 5 + - logger.log: "=== Rapid if test published 5 values ===" + +logger: + level: DEBUG + +globals: + - id: continuation_test_counter + type: int + restore_value: false + initial_value: '0' + +# Sensor to test rapid automation triggers with if/else (ContinuationAction) +sensor: + - platform: template + id: rapid_sensor + on_value: + - if: + condition: + lambda: 'return x > 2;' + then: + - logger.log: + format: "rapid-if-then: value=%d" + args: ['(int)x'] + else: + - logger.log: + format: "rapid-if-else: value=%d" + args: ['(int)x'] + - logger.log: + format: "rapid-if-completed: value=%d" + args: ['(int)x'] diff --git a/tests/integration/test_continuation_actions.py b/tests/integration/test_continuation_actions.py new file mode 100644 index 0000000000..1069ee7581 --- /dev/null +++ b/tests/integration/test_continuation_actions.py @@ -0,0 +1,235 @@ +"""Test continuation actions (ContinuationAction, WhileLoopContinuation, RepeatLoopContinuation).""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_continuation_actions( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """ + Test that continuation actions work correctly for if/while/repeat. + + These continuation classes replace LambdaAction with simple parent pointers, + saving 32-36 bytes per instance and eliminating std::function overhead. + """ + loop = asyncio.get_running_loop() + + # Track test completions + test_results = { + "if_then": False, + "if_else": False, + "if_complete": False, + "nested_both_true": False, + "nested_outer_true_inner_false": False, + "nested_outer_false": False, + "nested_complete": False, + "while_iterations": 0, + "while_complete": False, + "repeat_iterations": 0, + "repeat_complete": False, + "combined_iterations": 0, + "combined_complete": False, + "rapid_then": 0, + "rapid_else": 0, + "rapid_complete": 0, + } + + # Patterns for log messages + if_then_pattern = re.compile(r"if-then executed: value=(\d+)") + if_else_pattern = re.compile(r"if-else executed: value=(\d+)") + if_complete_pattern = re.compile(r"if completed") + nested_both_true_pattern = re.compile(r"nested-both-true") + nested_outer_true_inner_false_pattern = re.compile(r"nested-outer-true-inner-false") + nested_outer_false_pattern = re.compile(r"nested-outer-false") + nested_complete_pattern = re.compile(r"nested if completed") + while_iteration_pattern = re.compile(r"while-iteration-(\d+)") + while_complete_pattern = re.compile(r"while completed") + repeat_iteration_pattern = re.compile(r"repeat-iteration-(\d+)") + repeat_complete_pattern = re.compile(r"repeat completed") + combined_pattern = re.compile(r"combined-repeat(\d+)-while(\d+)") + combined_complete_pattern = re.compile(r"combined completed") + rapid_then_pattern = re.compile(r"rapid-if-then: value=(\d+)") + rapid_else_pattern = re.compile(r"rapid-if-else: value=(\d+)") + rapid_complete_pattern = re.compile(r"rapid-if-completed: value=(\d+)") + + # Test completion futures + test1_complete = loop.create_future() # if action + test2_complete = loop.create_future() # nested if + test3_complete = loop.create_future() # while + test4_complete = loop.create_future() # repeat + test5_complete = loop.create_future() # combined + test6_complete = loop.create_future() # rapid + + def check_output(line: str) -> None: + """Check log output for test messages.""" + # Test 1: IfAction + if if_then_pattern.search(line): + test_results["if_then"] = True + if if_else_pattern.search(line): + test_results["if_else"] = True + if if_complete_pattern.search(line): + test_results["if_complete"] = True + if not test1_complete.done(): + test1_complete.set_result(True) + + # Test 2: Nested IfAction + if nested_both_true_pattern.search(line): + test_results["nested_both_true"] = True + if nested_outer_true_inner_false_pattern.search(line): + test_results["nested_outer_true_inner_false"] = True + if nested_outer_false_pattern.search(line): + test_results["nested_outer_false"] = True + if nested_complete_pattern.search(line): + test_results["nested_complete"] = True + if not test2_complete.done(): + test2_complete.set_result(True) + + # Test 3: WhileAction + if match := while_iteration_pattern.search(line): + test_results["while_iterations"] = max( + test_results["while_iterations"], int(match.group(1)) + 1 + ) + if while_complete_pattern.search(line): + test_results["while_complete"] = True + if not test3_complete.done(): + test3_complete.set_result(True) + + # Test 4: RepeatAction + if match := repeat_iteration_pattern.search(line): + test_results["repeat_iterations"] = max( + test_results["repeat_iterations"], int(match.group(1)) + 1 + ) + if repeat_complete_pattern.search(line): + test_results["repeat_complete"] = True + if not test4_complete.done(): + test4_complete.set_result(True) + + # Test 5: Combined + if combined_pattern.search(line): + test_results["combined_iterations"] += 1 + if combined_complete_pattern.search(line): + test_results["combined_complete"] = True + if not test5_complete.done(): + test5_complete.set_result(True) + + # Test 6: Rapid triggers + if rapid_then_pattern.search(line): + test_results["rapid_then"] += 1 + if rapid_else_pattern.search(line): + test_results["rapid_else"] += 1 + if rapid_complete_pattern.search(line): + test_results["rapid_complete"] += 1 + if test_results["rapid_complete"] == 5 and not test6_complete.done(): + test6_complete.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Get services + _, services = await client.list_entities_services() + + # Test 1: IfAction with then branch + test_service = next((s for s in services if s.name == "test_if_action"), None) + assert test_service is not None, "test_if_action service not found" + client.execute_service(test_service, {"condition": True, "value": 42}) + await asyncio.wait_for(test1_complete, timeout=2.0) + assert test_results["if_then"], "IfAction then branch not executed" + assert test_results["if_complete"], "IfAction did not complete" + + # Test 1b: IfAction with else branch + test1_complete = loop.create_future() + test_results["if_complete"] = False + client.execute_service(test_service, {"condition": False, "value": 99}) + await asyncio.wait_for(test1_complete, timeout=2.0) + assert test_results["if_else"], "IfAction else branch not executed" + assert test_results["if_complete"], "IfAction did not complete" + + # Test 2: Nested IfAction - test all branches + test_service = next((s for s in services if s.name == "test_nested_if"), None) + assert test_service is not None, "test_nested_if service not found" + + # Both true + client.execute_service(test_service, {"outer": True, "inner": True}) + await asyncio.wait_for(test2_complete, timeout=2.0) + assert test_results["nested_both_true"], "Nested both true not executed" + + # Outer true, inner false + test2_complete = loop.create_future() + test_results["nested_complete"] = False + client.execute_service(test_service, {"outer": True, "inner": False}) + await asyncio.wait_for(test2_complete, timeout=2.0) + assert test_results["nested_outer_true_inner_false"], ( + "Nested outer true inner false not executed" + ) + + # Outer false + test2_complete = loop.create_future() + test_results["nested_complete"] = False + client.execute_service(test_service, {"outer": False, "inner": True}) + await asyncio.wait_for(test2_complete, timeout=2.0) + assert test_results["nested_outer_false"], "Nested outer false not executed" + + # Test 3: WhileAction + test_service = next( + (s for s in services if s.name == "test_while_action"), None + ) + assert test_service is not None, "test_while_action service not found" + client.execute_service(test_service, {"max_count": 3}) + await asyncio.wait_for(test3_complete, timeout=2.0) + assert test_results["while_iterations"] == 3, ( + f"WhileAction expected 3 iterations, got {test_results['while_iterations']}" + ) + assert test_results["while_complete"], "WhileAction did not complete" + + # Test 4: RepeatAction + test_service = next( + (s for s in services if s.name == "test_repeat_action"), None + ) + assert test_service is not None, "test_repeat_action service not found" + client.execute_service(test_service, {"count": 5}) + await asyncio.wait_for(test4_complete, timeout=2.0) + assert test_results["repeat_iterations"] == 5, ( + f"RepeatAction expected 5 iterations, got {test_results['repeat_iterations']}" + ) + assert test_results["repeat_complete"], "RepeatAction did not complete" + + # Test 5: Combined (if + repeat + while) + test_service = next((s for s in services if s.name == "test_combined"), None) + assert test_service is not None, "test_combined service not found" + client.execute_service(test_service, {"do_loop": True, "loop_count": 2}) + await asyncio.wait_for(test5_complete, timeout=2.0) + # Should execute: repeat 2 times, each iteration does while from iteration down to 0 + # iteration 0: while 0 times = 0 + # iteration 1: while 1 time = 1 + # Total: 1 combined log + assert test_results["combined_iterations"] >= 1, ( + f"Combined expected >=1 iterations, got {test_results['combined_iterations']}" + ) + assert test_results["combined_complete"], "Combined did not complete" + + # Test 6: Rapid triggers (tests memory efficiency of ContinuationAction) + test_service = next((s for s in services if s.name == "test_rapid_if"), None) + assert test_service is not None, "test_rapid_if service not found" + client.execute_service(test_service, {}) + await asyncio.wait_for(test6_complete, timeout=2.0) + # Values 1, 2 should hit else (<=2), values 3, 4, 5 should hit then (>2) + assert test_results["rapid_else"] == 2, ( + f"Rapid test expected 2 else, got {test_results['rapid_else']}" + ) + assert test_results["rapid_then"] == 3, ( + f"Rapid test expected 3 then, got {test_results['rapid_then']}" + ) + assert test_results["rapid_complete"] == 5, ( + f"Rapid test expected 5 completions, got {test_results['rapid_complete']}" + ) From 52a5cccc77f3f8845bb061f69d0763c382bd4098 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 17:39:57 -0600 Subject: [PATCH 394/394] fix regression from moved code that was conflicted --- esphome/core/base_automation.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index a31c501060..78838c70c8 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -330,9 +330,7 @@ template class WhileAction : public Action { template void WhileLoopContinuation::play(Ts... x) { if (this->parent_->num_running_ > 0 && this->parent_->condition_->check(x...)) { // play again - if (this->parent_->num_running_ > 0) { - this->parent_->then_.play(x...); - } + this->parent_->then_.play(x...); } else { // condition false, play next this->parent_->play_next_(x...);