From bc296d05fb83c901d8f36ac11c971ab94d4ed1db Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 16:57:18 -1000 Subject: [PATCH] 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