From bc296d05fb83c901d8f36ac11c971ab94d4ed1db Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 16:57:18 -1000 Subject: [PATCH 01/37] 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 02/37] 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 03/37] 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 04/37] 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 05/37] 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 06/37] 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 07/37] 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 08/37] 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 09/37] [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 10/37] 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 11/37] 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 12/37] [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 13/37] 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 14/37] 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 15/37] [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 16/37] 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 17/37] [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 18/37] 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 19/37] 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 20/37] 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 21/37] 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 22/37] 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 23/37] 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 24/37] 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 25/37] 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 26/37] 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 27/37] 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 28/37] 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 29/37] 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 30/37] 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 31/37] 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 32/37] [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 33/37] [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 34/37] [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 35/37] [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 36/37] [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 37/37] 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