From 6a96e0ee9073fbef36139818adc434833de94010 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 18 Oct 2025 09:38:37 -1000 Subject: [PATCH 1/9] [light] Use bitmask instead of std::set for color modes --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_connection.cpp | 6 +- esphome/components/api/api_pb2.cpp | 6 +- esphome/components/api/api_pb2.h | 2 +- esphome/components/api/api_pb2_dump.cpp | 2 +- esphome/components/light/color_mode.h | 127 ++++++++++++++++++++++ esphome/components/light/light_call.cpp | 8 +- esphome/components/light/light_call.h | 3 +- esphome/components/light/light_traits.h | 24 ++-- 9 files changed, 151 insertions(+), 29 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 753adc3592..4c1de4c4f5 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -506,7 +506,7 @@ message ListEntitiesLightResponse { string name = 3; reserved 4; // Deprecated: was string unique_id - repeated ColorMode supported_color_modes = 12 [(container_pointer) = "std::set"]; + repeated ColorMode supported_color_modes = 12 [(fixed_vector) = true]; // next four supports_* are for legacy clients, newer clients should use color modes // Deprecated in API version 1.6 bool legacy_supports_brightness = 5 [deprecated=true]; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 1f3456a205..32b0f0d953 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -477,7 +477,11 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c auto *light = static_cast(entity); ListEntitiesLightResponse msg; auto traits = light->get_traits(); - msg.supported_color_modes = &traits.get_supported_color_modes_for_api_(); + const auto &color_modes_mask = traits.get_supported_color_modes(); + msg.supported_color_modes.init(color_modes_mask.size()); + for (auto mode : color_modes_mask) { + msg.supported_color_modes.push_back(static_cast(mode)); + } if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) { msg.min_mireds = traits.get_min_mireds(); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 37bcf5d8a0..6bc434b658 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -471,7 +471,7 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name_ref_); - for (const auto &it : *this->supported_color_modes) { + for (auto &it : this->supported_color_modes) { buffer.encode_uint32(12, static_cast(it), true); } buffer.encode_float(9, this->min_mireds); @@ -492,8 +492,8 @@ void ListEntitiesLightResponse::calculate_size(ProtoSize &size) const { size.add_length(1, this->object_id_ref_.size()); size.add_fixed32(1, this->key); size.add_length(1, this->name_ref_.size()); - if (!this->supported_color_modes->empty()) { - for (const auto &it : *this->supported_color_modes) { + if (!this->supported_color_modes.empty()) { + for (const auto &it : this->supported_color_modes) { size.add_uint32_force(1, static_cast(it)); } } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 5603204801..528b8fc108 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -790,7 +790,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_light_response"; } #endif - const std::set *supported_color_modes{}; + FixedVector supported_color_modes{}; float min_mireds{0.0f}; float max_mireds{0.0f}; std::vector effects{}; diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index e803125f53..cda68ceee8 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -913,7 +913,7 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { dump_field(out, "object_id", this->object_id_ref_); dump_field(out, "key", this->key); dump_field(out, "name", this->name_ref_); - for (const auto &it : *this->supported_color_modes) { + for (const auto &it : this->supported_color_modes) { dump_field(out, "supported_color_modes", static_cast(it), 4); } dump_field(out, "min_mireds", this->min_mireds); diff --git a/esphome/components/light/color_mode.h b/esphome/components/light/color_mode.h index e524763c9f..d58ab73fdf 100644 --- a/esphome/components/light/color_mode.h +++ b/esphome/components/light/color_mode.h @@ -104,5 +104,132 @@ constexpr ColorModeHelper operator|(ColorModeHelper lhs, ColorMode rhs) { return static_cast(static_cast(lhs) | static_cast(rhs)); } +/// 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)); } + + constexpr bool contains(ColorMode mode) const { return (this->mask_ & (1 << mode_to_bit(mode))) != 0; } + + constexpr size_t size() const { + // Count set bits + uint16_t n = this->mask_; + size_t count = 0; + while (n) { + count += n & 1; + n >>= 1; + } + return count; + } + + /// 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(uint16_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() { + while (bit_ < 16 && !(mask_ & (1 << bit_))) { + ++bit_; + } + } + + uint16_t mask_; + int bit_; + }; + + constexpr Iterator begin() const { return Iterator(mask_, 0); } + constexpr Iterator end() const { return Iterator(mask_, 16); } + + private: + uint16_t mask_{0}; + + /// Map ColorMode enum values to bit positions (0-9) + static constexpr int mode_to_bit(ColorMode mode) { + // Using switch instead of lookup table to avoid RAM usage on ESP8266 + // The compiler optimizes this efficiently + switch (mode) { + case ColorMode::UNKNOWN: + return 0; + case ColorMode::ON_OFF: + return 1; + case ColorMode::BRIGHTNESS: + return 2; + case ColorMode::WHITE: + return 3; + case ColorMode::COLOR_TEMPERATURE: + return 4; + case ColorMode::COLD_WARM_WHITE: + return 5; + case ColorMode::RGB: + return 6; + case ColorMode::RGB_WHITE: + return 7; + case ColorMode::RGB_COLOR_TEMPERATURE: + return 8; + case ColorMode::RGB_COLD_WARM_WHITE: + return 9; + default: + return 0; + } + } + + static constexpr ColorMode bit_to_mode(int bit) { + // Using switch instead of lookup table to avoid RAM usage on ESP8266 + switch (bit) { + case 0: + return ColorMode::UNKNOWN; + case 1: + return ColorMode::ON_OFF; + case 2: + return ColorMode::BRIGHTNESS; + case 3: + return ColorMode::WHITE; + case 4: + return ColorMode::COLOR_TEMPERATURE; + case 5: + return ColorMode::COLD_WARM_WHITE; + case 6: + return ColorMode::RGB; + case 7: + return ColorMode::RGB_WHITE; + case 8: + return ColorMode::RGB_COLOR_TEMPERATURE; + case 9: + return ColorMode::RGB_COLD_WARM_WHITE; + default: + return ColorMode::UNKNOWN; + } + } +}; + } // namespace light } // namespace esphome diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 915b8fdf89..3e4e449614 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -425,10 +425,10 @@ ColorMode LightCall::compute_color_mode_() { // If no color mode is specified, we try to guess the color mode. This is needed for backward compatibility to // pre-colormode clients and automations, but also for the MQTT API, where HA doesn't let us know which color mode // was used for some reason. - std::set suitable_modes = this->get_suitable_color_modes_(); + ColorModeMask suitable_modes = this->get_suitable_color_modes_(); // Don't change if the current mode is suitable. - if (suitable_modes.count(current_mode) > 0) { + if (suitable_modes.contains(current_mode)) { ESP_LOGI(TAG, "'%s': color mode not specified; retaining %s", this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(current_mode))); return current_mode; @@ -436,7 +436,7 @@ ColorMode LightCall::compute_color_mode_() { // Use the preferred suitable mode. for (auto mode : suitable_modes) { - if (supported_modes.count(mode) == 0) + if (!supported_modes.contains(mode)) continue; ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(), @@ -451,7 +451,7 @@ ColorMode LightCall::compute_color_mode_() { LOG_STR_ARG(color_mode_to_human(color_mode))); return color_mode; } -std::set LightCall::get_suitable_color_modes_() { +ColorModeMask LightCall::get_suitable_color_modes_() { bool has_white = this->has_white() && this->white_ > 0.0f; bool has_ct = this->has_color_temperature(); bool has_cwww = diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h index d3a526b136..e87ccd3efd 100644 --- a/esphome/components/light/light_call.h +++ b/esphome/components/light/light_call.h @@ -1,7 +1,6 @@ #pragma once #include "light_color_values.h" -#include namespace esphome { @@ -187,7 +186,7 @@ class LightCall { //// Compute the color mode that should be used for this call. ColorMode compute_color_mode_(); /// Get potential color modes for this light call. - std::set get_suitable_color_modes_(); + ColorModeMask get_suitable_color_modes_(); /// Some color modes also can be set using non-native parameters, transform those calls. void transform_parameters_(); diff --git a/esphome/components/light/light_traits.h b/esphome/components/light/light_traits.h index a45301d148..94f1301694 100644 --- a/esphome/components/light/light_traits.h +++ b/esphome/components/light/light_traits.h @@ -2,7 +2,6 @@ #include "esphome/core/helpers.h" #include "color_mode.h" -#include namespace esphome { @@ -19,12 +18,15 @@ class LightTraits { public: LightTraits() = default; - const std::set &get_supported_color_modes() const { return this->supported_color_modes_; } - void set_supported_color_modes(std::set supported_color_modes) { - this->supported_color_modes_ = std::move(supported_color_modes); + const ColorModeMask &get_supported_color_modes() const { return this->supported_color_modes_; } + void set_supported_color_modes(ColorModeMask supported_color_modes) { + this->supported_color_modes_ = supported_color_modes; + } + void set_supported_color_modes(std::initializer_list modes) { + this->supported_color_modes_ = ColorModeMask(modes); } - bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.count(color_mode); } + bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.contains(color_mode); } bool supports_color_capability(ColorCapability color_capability) const { for (auto mode : this->supported_color_modes_) { if (mode & color_capability) @@ -59,17 +61,7 @@ class LightTraits { void set_max_mireds(float max_mireds) { this->max_mireds_ = max_mireds; } protected: -#ifdef USE_API - // The API connection is a friend class to access internal methods - friend class api::APIConnection; - // This method returns a reference to the internal color modes set. - // It is used by the API to avoid copying data when encoding messages. - // Warning: Do not use this method outside of the API connection code. - // It returns a reference to internal data that can be invalidated. - const std::set &get_supported_color_modes_for_api_() const { return this->supported_color_modes_; } -#endif - - std::set supported_color_modes_{}; + ColorModeMask supported_color_modes_{}; float min_mireds_{0}; float max_mireds_{0}; }; From c76e386a79d16a2935f69c10fe352d2fb9fd07e9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 18 Oct 2025 09:59:24 -1000 Subject: [PATCH 2/9] no vector --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_options.proto | 9 +++++ esphome/components/api/api_pb2.cpp | 14 +++++--- esphome/components/api/api_pb2.h | 2 +- esphome/components/api/api_pb2_dump.cpp | 6 ++-- script/api_protobuf/api_protobuf.py | 42 ++++++++++++++++++++++++ 6 files changed, 65 insertions(+), 10 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 4c1de4c4f5..c64fc038d6 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -506,7 +506,7 @@ message ListEntitiesLightResponse { string name = 3; reserved 4; // Deprecated: was string unique_id - repeated ColorMode supported_color_modes = 12 [(fixed_vector) = true]; + repeated ColorMode supported_color_modes = 12 [(enum_as_bitmask) = true]; // next four supports_* are for legacy clients, newer clients should use color modes // Deprecated in API version 1.6 bool legacy_supports_brightness = 5 [deprecated=true]; diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index ead8ac0bbc..450b5e83de 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -70,4 +70,13 @@ extend google.protobuf.FieldOptions { // init(size) before adding elements. This eliminates std::vector template overhead // and is ideal when the exact size is known before populating the array. optional bool fixed_vector = 50013 [default=false]; + + // enum_as_bitmask: Encode repeated enum fields as a uint32_t bitmask + // When set on a repeated enum field, the field will be stored as a single uint32_t + // where each bit represents whether that enum value is present. This is ideal for + // enums with ≤32 values and eliminates all vector template instantiation overhead. + // The enum values should be sequential starting from 0. + // Encoding: bit N set means enum value N is present in the set. + // Example: {ColorMode::RGB, ColorMode::WHITE} → bitmask with bits 5 and 6 set + optional bool enum_as_bitmask = 50014 [default=false]; } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 6bc434b658..c7b88bb312 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -471,8 +471,10 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name_ref_); - for (auto &it : this->supported_color_modes) { - buffer.encode_uint32(12, static_cast(it), true); + for (uint8_t bit = 0; bit < 32; bit++) { + if (this->supported_color_modes & (1U << bit)) { + buffer.encode_uint32(12, bit, true); + } } buffer.encode_float(9, this->min_mireds); buffer.encode_float(10, this->max_mireds); @@ -492,9 +494,11 @@ void ListEntitiesLightResponse::calculate_size(ProtoSize &size) const { size.add_length(1, this->object_id_ref_.size()); size.add_fixed32(1, this->key); size.add_length(1, this->name_ref_.size()); - if (!this->supported_color_modes.empty()) { - for (const auto &it : this->supported_color_modes) { - size.add_uint32_force(1, static_cast(it)); + if (this->supported_color_modes != 0) { + for (uint8_t bit = 0; bit < 32; bit++) { + if (this->supported_color_modes & (1U << bit)) { + size.add_uint32_force(1, static_cast(bit)); + } } } size.add_float(1, this->min_mireds); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 528b8fc108..5b86b7f276 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -790,7 +790,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_light_response"; } #endif - FixedVector supported_color_modes{}; + uint32_t supported_color_modes{}; float min_mireds{0.0f}; float max_mireds{0.0f}; std::vector effects{}; diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index cda68ceee8..f9f45ad071 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -913,9 +913,9 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { dump_field(out, "object_id", this->object_id_ref_); dump_field(out, "key", this->key); dump_field(out, "name", this->name_ref_); - for (const auto &it : this->supported_color_modes) { - dump_field(out, "supported_color_modes", static_cast(it), 4); - } + out.append(" supported_color_modes: 0x"); + out.append(uint32_to_string(this->supported_color_modes)); + out.append("\n"); dump_field(out, "min_mireds", this->min_mireds); dump_field(out, "max_mireds", this->max_mireds); for (const auto &it : this->effects) { diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 4936434fc2..9e140ca9ce 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1418,6 +1418,8 @@ class RepeatedTypeInfo(TypeInfo): self._use_pointer = bool(self._container_type) # Check if this should use FixedVector instead of std::vector self._use_fixed_vector = get_field_opt(field, pb.fixed_vector, False) + # Check if this should be encoded as a bitmask + self._use_bitmask = get_field_opt(field, pb.enum_as_bitmask, False) # For repeated fields, we need to get the base type info # but we can't call create_field_type_info as it would cause recursion @@ -1434,6 +1436,9 @@ class RepeatedTypeInfo(TypeInfo): @property def cpp_type(self) -> str: + if self._use_bitmask: + # For bitmask fields, store as a single uint32_t + return "uint32_t" if self._use_pointer and self._container_type: # For pointer fields, use the specified container type # If the container type already includes the element type (e.g., std::set) @@ -1466,6 +1471,12 @@ class RepeatedTypeInfo(TypeInfo): # Pointer fields don't support decoding if self._use_pointer: return None + if self._use_bitmask: + # For bitmask fields, decode enum value and set corresponding bit + content = self._ti.decode_varint + if content is None: + return None + return f"case {self.number}: this->{self.field_name} |= (1U << static_cast({content})); break;" content = self._ti.decode_varint if content is None: return None @@ -1519,6 +1530,18 @@ class RepeatedTypeInfo(TypeInfo): @property def encode_content(self) -> str: + if self._use_bitmask: + # For bitmask fields, iterate through set bits and encode each enum value + # The bitmask is stored as uint32_t where bit N represents enum value N + assert isinstance(self._ti, EnumType), ( + "enum_as_bitmask only works with enum fields" + ) + o = "for (uint8_t bit = 0; bit < 32; bit++) {\n" + o += f" if (this->{self.field_name} & (1U << bit)) {{\n" + o += f" buffer.{self._ti.encode_func}({self.number}, bit, true);\n" + o += " }\n" + o += "}" + return o if self._use_pointer: # For pointer fields, just dereference (pointer should never be null in our use case) o = f"for (const auto &it : *this->{self.field_name}) {{\n" @@ -1538,6 +1561,13 @@ class RepeatedTypeInfo(TypeInfo): @property def dump_content(self) -> str: + if self._use_bitmask: + # For bitmask fields, dump the hex value of the bitmask + return ( + f'out.append(" {self.field_name}: 0x");\n' + f"out.append(uint32_to_string(this->{self.field_name}));\n" + f'out.append("\\n");' + ) if self._use_pointer: # For pointer fields, dereference and use the existing helper return _generate_array_dump_content( @@ -1554,6 +1584,18 @@ class RepeatedTypeInfo(TypeInfo): # For repeated fields, we always need to pass force=True to the underlying type's calculation # This is because the encode method always sets force=true for repeated fields + if self._use_bitmask: + # For bitmask fields, iterate through set bits and calculate size + # Each set bit encodes one enum value (as varint) + o = f"if ({name} != 0) {{\n" + o += " for (uint8_t bit = 0; bit < 32; bit++) {\n" + o += f" if ({name} & (1U << bit)) {{\n" + o += f" {self._ti.get_size_calculation('bit', True)}\n" + o += " }\n" + o += " }\n" + o += "}" + return o + # Handle message types separately as they use a dedicated helper if isinstance(self._ti, MessageType): field_id_size = self._ti.calculate_field_id_size() From b01ab914f3ea0077d17a2554b925666e1f7bad9a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 18 Oct 2025 10:01:39 -1000 Subject: [PATCH 3/9] tweak --- esphome/components/api/api_connection.cpp | 5 +---- esphome/components/light/color_mode.h | 3 +++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 32b0f0d953..2a570b53e8 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -478,10 +478,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c ListEntitiesLightResponse msg; auto traits = light->get_traits(); const auto &color_modes_mask = traits.get_supported_color_modes(); - msg.supported_color_modes.init(color_modes_mask.size()); - for (auto mode : color_modes_mask) { - msg.supported_color_modes.push_back(static_cast(mode)); - } + msg.supported_color_modes = color_modes_mask.get_mask(); if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) { msg.min_mireds = traits.get_min_mireds(); diff --git a/esphome/components/light/color_mode.h b/esphome/components/light/color_mode.h index d58ab73fdf..fa3a0aaaac 100644 --- a/esphome/components/light/color_mode.h +++ b/esphome/components/light/color_mode.h @@ -169,6 +169,9 @@ class ColorModeMask { constexpr Iterator begin() const { return Iterator(mask_, 0); } constexpr Iterator end() const { return Iterator(mask_, 16); } + /// Get the raw bitmask value for API encoding + constexpr uint16_t get_mask() const { return this->mask_; } + private: uint16_t mask_{0}; From c0c30ba22dbe7fa4c687e0d80153865d2f8763f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 18 Oct 2025 10:02:45 -1000 Subject: [PATCH 4/9] tweak --- esphome/components/api/api_connection.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 2a570b53e8..74509691af 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -477,8 +477,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c auto *light = static_cast(entity); ListEntitiesLightResponse msg; auto traits = light->get_traits(); - const auto &color_modes_mask = traits.get_supported_color_modes(); - msg.supported_color_modes = color_modes_mask.get_mask(); + msg.supported_color_modes = traits.get_supported_color_modes().get_mask(); if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) { msg.min_mireds = traits.get_min_mireds(); From 2dc6c56edce33e3f10f0c214294f729b29b3dfb0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 18 Oct 2025 10:15:32 -1000 Subject: [PATCH 5/9] align --- esphome/components/light/light_traits.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/light/light_traits.h b/esphome/components/light/light_traits.h index 94f1301694..0db028598c 100644 --- a/esphome/components/light/light_traits.h +++ b/esphome/components/light/light_traits.h @@ -61,9 +61,9 @@ class LightTraits { void set_max_mireds(float max_mireds) { this->max_mireds_ = max_mireds; } protected: - ColorModeMask supported_color_modes_{}; float min_mireds_{0}; float max_mireds_{0}; + ColorModeMask supported_color_modes_{}; }; } // namespace light From 599e636468758f9b415b0963eb0f314a88895839 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 18 Oct 2025 10:17:52 -1000 Subject: [PATCH 6/9] comment --- esphome/components/light/color_mode.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/light/color_mode.h b/esphome/components/light/color_mode.h index fa3a0aaaac..d9fdc24d35 100644 --- a/esphome/components/light/color_mode.h +++ b/esphome/components/light/color_mode.h @@ -173,6 +173,10 @@ class ColorModeMask { constexpr uint16_t get_mask() const { return this->mask_; } 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). uint16_t mask_{0}; /// Map ColorMode enum values to bit positions (0-9) From 957b5e98a78a00002ba1cdc2e4f44071e8ff667a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 18 Oct 2025 10:18:34 -1000 Subject: [PATCH 7/9] comment --- esphome/components/light/color_mode.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/light/color_mode.h b/esphome/components/light/color_mode.h index d9fdc24d35..77be58bb3b 100644 --- a/esphome/components/light/color_mode.h +++ b/esphome/components/light/color_mode.h @@ -141,13 +141,13 @@ class ColorModeMask { using pointer = const ColorMode *; using reference = ColorMode; - constexpr Iterator(uint16_t mask, int bit) : mask_(mask), bit_(bit) { advance_to_next_set_bit(); } + constexpr Iterator(uint16_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(); + advance_to_next_set_bit_(); return *this; } @@ -156,7 +156,7 @@ class ColorModeMask { constexpr bool operator!=(const Iterator &other) const { return !(*this == other); } private: - constexpr void advance_to_next_set_bit() { + constexpr void advance_to_next_set_bit_() { while (bit_ < 16 && !(mask_ & (1 << bit_))) { ++bit_; } From cfb061abc423965b1a2474ce50c3e6e0ebb0c0c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 18 Oct 2025 10:29:08 -1000 Subject: [PATCH 8/9] preen --- esphome/components/api/api_connection.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 74509691af..f7ee0619c5 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -453,7 +453,6 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection * bool is_single) { auto *light = static_cast(entity); LightStateResponse resp; - auto traits = light->get_traits(); auto values = light->remote_values; auto color_mode = values.get_color_mode(); resp.state = values.is_on(); From 98df9fd2ff49f1750c8255e7de18b0c478ab73d9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 18 Oct 2025 10:32:20 -1000 Subject: [PATCH 9/9] preen --- esphome/components/light/light_call.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 3e4e449614..4e6251492d 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -406,7 +406,7 @@ void LightCall::transform_parameters_() { } } ColorMode LightCall::compute_color_mode_() { - auto supported_modes = this->parent_->get_traits().get_supported_color_modes(); + const auto &supported_modes = this->parent_->get_traits().get_supported_color_modes(); int supported_count = supported_modes.size(); // Some lights don't support any color modes (e.g. monochromatic light), leave it at unknown.