From d27e78e909be543bfc6e779ff988de6526686d9d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 04:13:34 -0700 Subject: [PATCH 1/9] [select] Store options in flash to reduce RAM usage --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_pb2.cpp | 8 +++---- esphome/components/api/api_pb2.h | 2 +- .../components/copy/select/copy_select.cpp | 3 ++- esphome/components/lvgl/lvgl_esphome.h | 2 +- esphome/components/lvgl/select/lvgl_select.h | 12 +++++++++- esphome/components/select/select.cpp | 5 ++-- esphome/components/select/select_traits.cpp | 4 ++-- esphome/components/select/select_traits.h | 10 ++++---- .../template/select/template_select.cpp | 2 +- esphome/core/helpers.h | 10 ++++++++ script/api_protobuf/api_protobuf.py | 24 +++++++++++++------ 12 files changed, 58 insertions(+), 26 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index d202486cfa..b12b53fd00 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1143,7 +1143,7 @@ message ListEntitiesSelectResponse { reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; - repeated string options = 6 [(container_pointer) = "std::vector"]; + repeated string options = 6 [(container_pointer_no_template) = "FixedVector"]; bool disabled_by_default = 7; EntityCategory entity_category = 8; uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 37bcf5d8a0..3472707d3c 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1475,8 +1475,8 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon_ref_); #endif - for (const auto &it : *this->options) { - buffer.encode_string(6, it, true); + for (const char *it : *this->options) { + buffer.encode_string(6, it, strlen(it), true); } buffer.encode_bool(7, this->disabled_by_default); buffer.encode_uint32(8, static_cast(this->entity_category)); @@ -1492,8 +1492,8 @@ void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const { size.add_length(1, this->icon_ref_.size()); #endif if (!this->options->empty()) { - for (const auto &it : *this->options) { - size.add_length_force(1, it.size()); + for (const char *it : *this->options) { + size.add_length_force(1, strlen(it)); } } size.add_bool(1, this->disabled_by_default); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index ed49498176..2f23201dcd 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1534,7 +1534,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_select_response"; } #endif - const std::vector *options{}; + const FixedVector *options{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/copy/select/copy_select.cpp b/esphome/components/copy/select/copy_select.cpp index bdcbd0b42c..6618ae6347 100644 --- a/esphome/components/copy/select/copy_select.cpp +++ b/esphome/components/copy/select/copy_select.cpp @@ -9,7 +9,8 @@ static const char *const TAG = "copy.select"; void CopySelect::setup() { source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); }); - traits.set_options(source_->traits.get_options()); + const auto &source_options = source_->traits.get_options(); + traits.set_options({source_options.begin(), source_options.end()}); if (source_->has_state()) this->publish_state(source_->state); diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 3ae67e8a0b..d3dc8fac5a 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -358,7 +358,7 @@ class LvSelectable : public LvCompound { virtual void set_selected_index(size_t index, lv_anim_enable_t anim) = 0; void set_selected_text(const std::string &text, lv_anim_enable_t anim); std::string get_selected_text(); - std::vector get_options() { return this->options_; } + const std::vector &get_options() { return this->options_; } void set_options(std::vector options); protected: diff --git a/esphome/components/lvgl/select/lvgl_select.h b/esphome/components/lvgl/select/lvgl_select.h index a0e60295a6..0ab28d372d 100644 --- a/esphome/components/lvgl/select/lvgl_select.h +++ b/esphome/components/lvgl/select/lvgl_select.h @@ -53,7 +53,17 @@ class LVGLSelect : public select::Select, public Component { this->widget_->set_selected_text(value, this->anim_); this->publish(); } - void set_options_() { this->traits.set_options(this->widget_->get_options()); } + void set_options_() { + // Widget uses std::vector, SelectTraits uses FixedVector + // Convert by extracting c_str() pointers + const auto &opts = this->widget_->get_options(); + std::vector opt_ptrs; + opt_ptrs.reserve(opts.size()); + for (const auto &opt : opts) { + opt_ptrs.push_back(opt.c_str()); + } + this->traits.set_options({opt_ptrs.begin(), opt_ptrs.end()}); + } LvSelectable *widget_; lv_anim_enable_t anim_; diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 16e8288ca1..66cd51e15a 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -1,5 +1,6 @@ #include "select.h" #include "esphome/core/log.h" +#include namespace esphome { namespace select { @@ -35,7 +36,7 @@ size_t Select::size() const { optional Select::index_of(const std::string &option) const { const auto &options = traits.get_options(); for (size_t i = 0; i < options.size(); i++) { - if (options[i] == option) { + if (strcmp(options[i], option.c_str()) == 0) { return i; } } @@ -53,7 +54,7 @@ optional Select::active_index() const { optional Select::at(size_t index) const { if (this->has_index(index)) { const auto &options = traits.get_options(); - return options.at(index); + return std::string(options.at(index)); } else { return {}; } diff --git a/esphome/components/select/select_traits.cpp b/esphome/components/select/select_traits.cpp index a8cd4290c8..06bd2404c2 100644 --- a/esphome/components/select/select_traits.cpp +++ b/esphome/components/select/select_traits.cpp @@ -3,9 +3,9 @@ namespace esphome { namespace select { -void SelectTraits::set_options(std::vector options) { this->options_ = std::move(options); } +void SelectTraits::set_options(std::initializer_list options) { this->options_ = options; } -const std::vector &SelectTraits::get_options() const { return this->options_; } +const FixedVector &SelectTraits::get_options() const { return this->options_; } } // namespace select } // namespace esphome diff --git a/esphome/components/select/select_traits.h b/esphome/components/select/select_traits.h index 128066dd6b..8f8fe3b71f 100644 --- a/esphome/components/select/select_traits.h +++ b/esphome/components/select/select_traits.h @@ -1,18 +1,18 @@ #pragma once -#include -#include +#include "esphome/core/helpers.h" +#include namespace esphome { namespace select { class SelectTraits { public: - void set_options(std::vector options); - const std::vector &get_options() const; + void set_options(std::initializer_list options); + const FixedVector &get_options() const; protected: - std::vector options_; + FixedVector options_; }; } // namespace select diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 95b0ee0d2b..7f7aa2c43f 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -22,7 +22,7 @@ void TemplateSelect::setup() { ESP_LOGD(TAG, "State from initial (could not load stored index): %s", value.c_str()); } else if (!this->has_index(index)) { value = this->initial_option_; - ESP_LOGD(TAG, "State from initial (restored index %d out of bounds): %s", index, value.c_str()); + ESP_LOGD(TAG, "State from initial (restored index %zu out of bounds): %s", index, value.c_str()); } else { value = this->at(index).value(); ESP_LOGD(TAG, "State from restore: %s", value.c_str()); diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 9b0591c9c5..7b4f2ad21f 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -304,6 +304,11 @@ template class FixedVector { return data_[size_ - 1]; } + /// Access first element (no bounds checking - matches std::vector behavior) + /// Caller must ensure vector is not empty (size() > 0) + T &front() { return data_[0]; } + const T &front() const { return data_[0]; } + /// Access last element (no bounds checking - matches std::vector behavior) /// Caller must ensure vector is not empty (size() > 0) T &back() { return data_[size_ - 1]; } @@ -317,6 +322,11 @@ template class FixedVector { T &operator[](size_t i) { return data_[i]; } const T &operator[](size_t i) const { return data_[i]; } + /// Access element with bounds checking (matches std::vector behavior) + /// Caller must ensure index is valid (i < size()) + T &at(size_t i) { return data_[i]; } + const T &at(size_t i) const { return data_[i]; } + // Iterator support for range-based for loops T *begin() { return data_; } T *end() { return data_ + size_; } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 2f83b0bd79..f5cca0e0de 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1533,11 +1533,16 @@ class RepeatedTypeInfo(TypeInfo): def encode_content(self) -> str: if self._use_pointer: # For pointer fields, just dereference (pointer should never be null in our use case) - o = f"for (const auto &it : *this->{self.field_name}) {{\n" - if isinstance(self._ti, EnumType): - o += f" buffer.{self._ti.encode_func}({self.number}, static_cast(it), true);\n" + # Special handling for const char* elements (when container_no_template contains "const char") + if "const char" in self._container_no_template: + o = f"for (const char *it : *this->{self.field_name}) {{\n" + o += f" buffer.{self._ti.encode_func}({self.number}, it, strlen(it), true);\n" else: - o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n" + o = f"for (const auto &it : *this->{self.field_name}) {{\n" + if isinstance(self._ti, EnumType): + o += f" buffer.{self._ti.encode_func}({self.number}, static_cast(it), true);\n" + else: + o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n" o += "}" return o o = f"for (auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{\n" @@ -1588,9 +1593,14 @@ class RepeatedTypeInfo(TypeInfo): o += f" size.add_precalculated_size({size_expr} * {bytes_per_element});\n" else: # Other types need the actual value - auto_ref = "" if self._ti_is_bool else "&" - o += f" for (const auto {auto_ref}it : {container_ref}) {{\n" - o += f" {self._ti.get_size_calculation('it', True)}\n" + # Special handling for const char* elements + if self._use_pointer and "const char" in self._container_no_template: + o += f" for (const char *it : {container_ref}) {{\n" + o += " size.add_length_force(1, strlen(it));\n" + else: + auto_ref = "" if self._ti_is_bool else "&" + o += f" for (const auto {auto_ref}it : {container_ref}) {{\n" + o += f" {self._ti.get_size_calculation('it', True)}\n" o += " }\n" o += "}" From 3d6224d1b10d4431bb0dacfe1fd74c9ab0fb71aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 04:22:22 -0700 Subject: [PATCH 2/9] [select] Store options in flash to reduce RAM usage --- esphome/components/copy/select/copy_select.cpp | 2 +- esphome/components/select/select_traits.cpp | 2 ++ esphome/components/select/select_traits.h | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/copy/select/copy_select.cpp b/esphome/components/copy/select/copy_select.cpp index 6618ae6347..be90af5a13 100644 --- a/esphome/components/copy/select/copy_select.cpp +++ b/esphome/components/copy/select/copy_select.cpp @@ -10,7 +10,7 @@ void CopySelect::setup() { source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); }); const auto &source_options = source_->traits.get_options(); - traits.set_options({source_options.begin(), source_options.end()}); + traits.set_options(source_options); if (source_->has_state()) this->publish_state(source_->state); diff --git a/esphome/components/select/select_traits.cpp b/esphome/components/select/select_traits.cpp index 06bd2404c2..90a70393d1 100644 --- a/esphome/components/select/select_traits.cpp +++ b/esphome/components/select/select_traits.cpp @@ -5,6 +5,8 @@ namespace select { void SelectTraits::set_options(std::initializer_list options) { this->options_ = options; } +void SelectTraits::set_options(const FixedVector &options) { this->options_ = options; } + const FixedVector &SelectTraits::get_options() const { return this->options_; } } // namespace select diff --git a/esphome/components/select/select_traits.h b/esphome/components/select/select_traits.h index 8f8fe3b71f..b504f08298 100644 --- a/esphome/components/select/select_traits.h +++ b/esphome/components/select/select_traits.h @@ -9,6 +9,7 @@ namespace select { class SelectTraits { public: void set_options(std::initializer_list options); + void set_options(const FixedVector &options); const FixedVector &get_options() const; protected: From 18b12f845dd15ed0dc1b8332765d0b81c1e3b46d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 04:22:52 -0700 Subject: [PATCH 3/9] [select] Store options in flash to reduce RAM usage --- esphome/components/lvgl/select/lvgl_select.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/lvgl/select/lvgl_select.h b/esphome/components/lvgl/select/lvgl_select.h index 0ab28d372d..3b1fd67d68 100644 --- a/esphome/components/lvgl/select/lvgl_select.h +++ b/esphome/components/lvgl/select/lvgl_select.h @@ -57,12 +57,12 @@ class LVGLSelect : public select::Select, public Component { // Widget uses std::vector, SelectTraits uses FixedVector // Convert by extracting c_str() pointers const auto &opts = this->widget_->get_options(); - std::vector opt_ptrs; - opt_ptrs.reserve(opts.size()); - for (const auto &opt : opts) { - opt_ptrs.push_back(opt.c_str()); + FixedVector opt_ptrs; + opt_ptrs.init(opts.size()); + for (size_t i = 0; i < opts.size(); i++) { + opt_ptrs[i] = opts[i].c_str(); } - this->traits.set_options({opt_ptrs.begin(), opt_ptrs.end()}); + this->traits.set_options(opt_ptrs); } LvSelectable *widget_; From 83e4013a259bfac890bba56bce403b42edd661a1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 04:27:41 -0700 Subject: [PATCH 4/9] [select] Store options in flash to reduce RAM usage --- esphome/components/copy/select/copy_select.cpp | 3 +-- esphome/components/select/select.cpp | 2 +- esphome/core/helpers.h | 5 ----- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/esphome/components/copy/select/copy_select.cpp b/esphome/components/copy/select/copy_select.cpp index be90af5a13..bdcbd0b42c 100644 --- a/esphome/components/copy/select/copy_select.cpp +++ b/esphome/components/copy/select/copy_select.cpp @@ -9,8 +9,7 @@ static const char *const TAG = "copy.select"; void CopySelect::setup() { source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); }); - const auto &source_options = source_->traits.get_options(); - traits.set_options(source_options); + traits.set_options(source_->traits.get_options()); if (source_->has_state()) this->publish_state(source_->state); diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 66cd51e15a..5961d71faa 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -54,7 +54,7 @@ optional Select::active_index() const { optional Select::at(size_t index) const { if (this->has_index(index)) { const auto &options = traits.get_options(); - return std::string(options.at(index)); + return std::string(options[index]); } else { return {}; } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 7b4f2ad21f..15f05b9b6f 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -322,11 +322,6 @@ template class FixedVector { T &operator[](size_t i) { return data_[i]; } const T &operator[](size_t i) const { return data_[i]; } - /// Access element with bounds checking (matches std::vector behavior) - /// Caller must ensure index is valid (i < size()) - T &at(size_t i) { return data_[i]; } - const T &at(size_t i) const { return data_[i]; } - // Iterator support for range-based for loops T *begin() { return data_; } T *end() { return data_ + size_; } From 09f97d86e68761ed8c79decd65f960b6b0ab055d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 04:31:16 -0700 Subject: [PATCH 5/9] [select] Store options in flash to reduce RAM usage --- esphome/components/select/select_traits.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/select/select_traits.cpp b/esphome/components/select/select_traits.cpp index 90a70393d1..dc849b8b7e 100644 --- a/esphome/components/select/select_traits.cpp +++ b/esphome/components/select/select_traits.cpp @@ -5,7 +5,12 @@ namespace select { void SelectTraits::set_options(std::initializer_list options) { this->options_ = options; } -void SelectTraits::set_options(const FixedVector &options) { this->options_ = options; } +void SelectTraits::set_options(const FixedVector &options) { + this->options_.init(options.size()); + for (size_t i = 0; i < options.size(); i++) { + this->options_[i] = options[i]; + } +} const FixedVector &SelectTraits::get_options() const { return this->options_; } From 3ae82f6b98014d9aa3409693660deddc2b2a484b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 04:39:55 -0700 Subject: [PATCH 6/9] [select] Store options in flash to reduce RAM usage --- esphome/components/api/api_pb2_dump.cpp | 6 ++++++ script/api_protobuf/api_protobuf.py | 27 ++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index e803125f53..d94ceaaa9c 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -88,6 +88,12 @@ static void dump_field(std::string &out, const char *field_name, StringRef value out.append("\n"); } +static void dump_field(std::string &out, const char *field_name, const char *value, int indent = 2) { + append_field_prefix(out, field_name, indent); + out.append("'").append(value).append("'"); + out.append("\n"); +} + template static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) { append_field_prefix(out, field_name, indent); out.append(proto_enum_to_string(value)); diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index f5cca0e0de..394e92b9a7 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1162,7 +1162,11 @@ class SInt64Type(TypeInfo): def _generate_array_dump_content( - ti, field_name: str, name: str, is_bool: bool = False + ti, + field_name: str, + name: str, + is_bool: bool = False, + is_const_char_ptr: bool = False, ) -> str: """Generate dump content for array types (repeated or fixed array). @@ -1170,7 +1174,10 @@ def _generate_array_dump_content( """ o = f"for (const auto {'' if is_bool else '&'}it : {field_name}) {{\n" # Check if underlying type can use dump_field - if ti.can_use_dump_field(): + if is_const_char_ptr: + # Special case for const char* - use it directly + o += f' dump_field(out, "{name}", it, 4);\n' + elif ti.can_use_dump_field(): # For types that have dump_field overloads, use them with extra indent # std::vector iterators return proxy objects, need explicit cast value_expr = "static_cast(it)" if is_bool else ti.dump_field_value("it") @@ -1555,10 +1562,18 @@ class RepeatedTypeInfo(TypeInfo): @property def dump_content(self) -> str: + # Check if this is const char* elements + is_const_char_ptr = ( + self._use_pointer and "const char" in self._container_no_template + ) if self._use_pointer: # For pointer fields, dereference and use the existing helper return _generate_array_dump_content( - self._ti, f"*this->{self.field_name}", self.name, is_bool=False + self._ti, + f"*this->{self.field_name}", + self.name, + is_bool=False, + is_const_char_ptr=is_const_char_ptr, ) return _generate_array_dump_content( self._ti, f"this->{self.field_name}", self.name, is_bool=self._ti_is_bool @@ -2552,6 +2567,12 @@ static void dump_field(std::string &out, const char *field_name, StringRef value out.append("\\n"); } +static void dump_field(std::string &out, const char *field_name, const char *value, int indent = 2) { + append_field_prefix(out, field_name, indent); + out.append("'").append(value).append("'"); + out.append("\\n"); +} + template static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) { append_field_prefix(out, field_name, indent); From 4135e0b5db28c7585594531e6d4b95dc11bec659 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 06:43:03 -0700 Subject: [PATCH 7/9] fixes --- .../modbus_controller/select/modbus_select.cpp | 10 ++++++---- esphome/components/tuya/select/tuya_select.cpp | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/modbus_controller/select/modbus_select.cpp b/esphome/components/modbus_controller/select/modbus_select.cpp index 56b8c783ed..674dd05e55 100644 --- a/esphome/components/modbus_controller/select/modbus_select.cpp +++ b/esphome/components/modbus_controller/select/modbus_select.cpp @@ -41,10 +41,12 @@ void ModbusSelect::parse_and_publish(const std::vector &data) { } void ModbusSelect::control(const std::string &value) { - auto options = this->traits.get_options(); - auto opt_it = std::find(options.cbegin(), options.cend(), value); - size_t idx = std::distance(options.cbegin(), opt_it); - optional mapval = this->mapping_[idx]; + auto idx = this->index_of(value); + if (!idx.has_value()) { + ESP_LOGW(TAG, "Invalid option '%s'", value.c_str()); + return; + } + optional mapval = this->mapping_[idx.value()]; ESP_LOGD(TAG, "Found value %lld for option '%s'", *mapval, value.c_str()); std::vector data; diff --git a/esphome/components/tuya/select/tuya_select.cpp b/esphome/components/tuya/select/tuya_select.cpp index 91ddbc77ec..7b175ee195 100644 --- a/esphome/components/tuya/select/tuya_select.cpp +++ b/esphome/components/tuya/select/tuya_select.cpp @@ -10,7 +10,7 @@ void TuyaSelect::setup() { this->parent_->register_listener(this->select_id_, [this](const TuyaDatapoint &datapoint) { uint8_t enum_value = datapoint.value_enum; ESP_LOGV(TAG, "MCU reported select %u value %u", this->select_id_, enum_value); - auto options = this->traits.get_options(); + const auto &options = this->traits.get_options(); auto mappings = this->mappings_; auto it = std::find(mappings.cbegin(), mappings.cend(), enum_value); if (it == mappings.end()) { @@ -49,9 +49,9 @@ void TuyaSelect::dump_config() { " Data type: %s\n" " Options are:", this->select_id_, this->is_int_ ? "int" : "enum"); - auto options = this->traits.get_options(); + const auto &options = this->traits.get_options(); for (size_t i = 0; i < this->mappings_.size(); i++) { - ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options.at(i).c_str()); + ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options[i]); } } From b2cded14ecfcf995df17855badf4a7fd9c33c92f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 06:46:54 -0700 Subject: [PATCH 8/9] tweak --- esphome/components/select/select.cpp | 2 +- esphome/components/tuya/select/tuya_select.cpp | 2 +- esphome/core/helpers.h | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 5961d71faa..66cd51e15a 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -54,7 +54,7 @@ optional Select::active_index() const { optional Select::at(size_t index) const { if (this->has_index(index)) { const auto &options = traits.get_options(); - return std::string(options[index]); + return std::string(options.at(index)); } else { return {}; } diff --git a/esphome/components/tuya/select/tuya_select.cpp b/esphome/components/tuya/select/tuya_select.cpp index 7b175ee195..d9dc532771 100644 --- a/esphome/components/tuya/select/tuya_select.cpp +++ b/esphome/components/tuya/select/tuya_select.cpp @@ -51,7 +51,7 @@ void TuyaSelect::dump_config() { this->select_id_, this->is_int_ ? "int" : "enum"); const auto &options = this->traits.get_options(); for (size_t i = 0; i < this->mappings_.size(); i++) { - ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options[i]); + ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options.at(i)); } } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 15f05b9b6f..cf21ddc16d 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -322,6 +322,11 @@ template class FixedVector { T &operator[](size_t i) { return data_[i]; } const T &operator[](size_t i) const { return data_[i]; } + /// Access element with bounds checking (matches std::vector behavior) + /// Note: No exception thrown on out of bounds - caller must ensure index is valid + T &at(size_t i) { return data_[i]; } + const T &at(size_t i) const { return data_[i]; } + // Iterator support for range-based for loops T *begin() { return data_; } T *end() { return data_ + size_; } From 44157f1ceddc224c4c5eaf0b6abcd3795c1be930 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Oct 2025 07:16:40 -0700 Subject: [PATCH 9/9] tweak --- esphome/components/modbus_controller/select/modbus_select.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/modbus_controller/select/modbus_select.cpp b/esphome/components/modbus_controller/select/modbus_select.cpp index 674dd05e55..4d4b5a4ffc 100644 --- a/esphome/components/modbus_controller/select/modbus_select.cpp +++ b/esphome/components/modbus_controller/select/modbus_select.cpp @@ -28,7 +28,7 @@ void ModbusSelect::parse_and_publish(const std::vector &data) { if (map_it != this->mapping_.cend()) { size_t idx = std::distance(this->mapping_.cbegin(), map_it); - new_state = this->traits.get_options()[idx]; + new_state = std::string(this->traits.get_options()[idx]); ESP_LOGV(TAG, "Found option %s for value %lld", new_state->c_str(), value); } else { ESP_LOGE(TAG, "No option found for mapping %lld", value);