Select Entity Class: Index-Based Operations and Flash Storage
ESPHome 2025.11.0 introduces significant optimizations to the Select entity class that reduce memory usage and improve performance. These changes affect external components that implement custom select entities.
Background
Section titled “Background”Two related PRs optimize the Select entity class:
PR #11623: Index-Based Operations
Refactors Select to use indices internally instead of strings, eliminating redundant string storage and operations. The public state member is deprecated and will be removed in ESPHome 2026.5.0 (6-month migration window). This saves ~32 bytes per SelectCall operation immediately, and will save at least 28 bytes per Select instance after the deprecated .state member is removed (28 bytes std::string overhead + string length).
PR #11514: Store Options in Flash
Changes option storage from heap-allocated std::vector<std::string> to flash-stored FixedVector<const char*>. Real device measurements show 164-7428 bytes saved, scaling with the total number of options across all select entities. More selects or more options per select means greater savings.
What’s Changing
Section titled “What’s Changing”For ESPHome 2025.11.0 and Later
Section titled “For ESPHome 2025.11.0 and Later”Storage Changes (Breaking - PR #11514):
// OLD - heap-allocated stringsstd::vector<std::string> options;traits.set_options(options);
// NEW - flash-stored string literalstraits.set_options({"Option 1", "Option 2", "Option 3"});State Access Changes (Deprecation - PR #11623):
// OLD - deprecated, shows warnings (works until 2026.5.0)std::string current = my_select->state;
// NEW - required after 2026.5.0const char *current = my_select->current_option();Who This Affects
Section titled “Who This Affects”This affects external components that:
- Manually call
set_options()on SelectTraits in C++ code (Python code generation already uses the correct syntax) - Access the
.statemember of Select objects - Iterate over or compare select options
Standard YAML configurations are not affected - Python code generation already produces initializer lists, so no YAML changes are needed. This only impacts external components that create select entities entirely in C++.
Migration Guide
Section titled “Migration Guide”1. Setting Options (Required Now)
Section titled “1. Setting Options (Required Now)”In setup() methods:
// OLDstd::vector<std::string> options = {"Low", "Medium", "High"};this->traits.set_options(options);
// NEW - use initializer list with string literalsthis->traits.set_options({"Low", "Medium", "High"});For runtime-determined options (rare), you must store the strings persistently:
#include "esphome/core/helpers.h" // For FixedVector
class MySelect : public select::Select { protected: // Storage for actual string data (must persist for lifetime) std::vector<std::string> stored_options_; // Pointers into stored_options_ FixedVector<const char*> option_ptrs_;
void setup() override { // Read dynamic options from device/config (truly runtime-determined) uint8_t mode_count = this->read_mode_count_from_device(); this->stored_options_.resize(mode_count); for (uint8_t i = 0; i < mode_count; i++) { this->stored_options_[i] = this->read_mode_name_from_device(i); }
// Build pointer array pointing into stored_options_ this->option_ptrs_.init(this->stored_options_.size()); for (const auto &opt : this->stored_options_) { this->option_ptrs_.push_back(opt.c_str()); }
// Set the traits (pointers remain valid because stored_options_ persists) this->traits.set_options(this->option_ptrs_); }};2. Accessing Options (Required Now)
Section titled “2. Accessing Options (Required Now)”Reading the options list:
// OLD - copying (deleted copy constructor)auto options = traits.get_options();
// NEW - use const referenceconst auto &options = traits.get_options();
// Individual options are now const char*const char *option = options[0]; // Not std::string
// If you need std::string:std::string str = std::string(options[0]);3. Reading Current Selection (Deprecated, Remove by 2026.5.0)
Section titled “3. Reading Current Selection (Deprecated, Remove by 2026.5.0)”In YAML lambdas:
# OLD - shows deprecation warning (works until 2026.5.0)lambda: 'return id(my_select).state == "option1";'
# NEW - required after 2026.5.0, use strcmp()lambda: 'return strcmp(id(my_select).current_option(), "option1") == 0;'
# Or convert to std::string if you prefer == operator (less efficient)lambda: 'return std::string(id(my_select).current_option()) == "option1";'In C++ code:
// OLD - deprecated (works until 2026.5.0)std::string current = my_select->state;ESP_LOGD(TAG, "Current: %s", my_select->state.c_str());
// NEW - required after 2026.5.0const char *current = my_select->current_option();ESP_LOGD(TAG, "Current: %s", current);
// If you need std::string:std::string current = my_select->current_option(); // Implicit conversion4. Publishing State (New Methods Available)
Section titled “4. Publishing State (New Methods Available)”Prefer index-based operations:
// OLD - string-based (still works but less efficient)this->publish_state("option1");
// NEW - index-based (more efficient)this->publish_state(0); // Publish by index5. String Comparisons
Section titled “5. String Comparisons”When comparing options:
// OLD - std::string comparisonif (options[i] == "value") { }
// NEW - use strcmp()if (strcmp(options[i], "value") == 0) { }
// BETTER - use Select helper methodsauto idx = this->index_of(value);if (idx.has_value()) { this->publish_state(idx.value());}6. Overriding control() Method (Required)
Section titled “6. Overriding control() Method (Required)”IMPORTANT: You must override at least one control() method. If you override neither, they will call each other infinitely.
class MySelect : public select::Select { protected: // Option 1: String-based control (still works, but less efficient) void control(const std::string &value) override { // This version receives the string value auto idx = this->index_of(value); // strcmp lookup needed if (idx.has_value()) { this->send_to_device(idx.value()); } }
// Option 2: Index-based control (preferred, more efficient) void control(size_t index) override { // This version receives the index directly this->send_to_device(index); // No lookup needed }};Which to override?
- Override
control(size_t index)(preferred) - avoids string conversions and lookups - Override
control(const std::string &value)- if you need the actual string value - Override both (rare) - if you need different handling for each case
Supporting Multiple ESPHome Versions
Section titled “Supporting Multiple ESPHome Versions”.state Member Access (Deprecated but Still Exists)
Section titled “.state Member Access (Deprecated but Still Exists)”The .state member still exists until 2026.5.0, so you can use version guards:
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0) const char *current = my_select->current_option();#else const char *current = my_select->state.c_str();#endifOptions Storage (Hard Breaking Change)
Section titled “Options Storage (Hard Breaking Change)”The old set_options(std::vector<std::string>) API was completely removed in PR #11514. Version guards are not possible because the old API no longer exists.
External components must either:
- Update to the new API to support ESPHome 2025.11.0+
- Pin to ESPHome versions before 2025.11.0 if they can’t update yet
There is no way to support both old and new ESPHome versions for options storage without maintaining separate branches.
Timeline
Section titled “Timeline”-
ESPHome 2025.11.0 (November 2025):
- Options storage change is active (breaking change)
.statemember deprecated but still works with warnings- New
current_option()method available
-
ESPHome 2026.5.0 (May 2026):
.statemember will be removed- Must use
current_option()method
Finding Code That Needs Updates
Section titled “Finding Code That Needs Updates”Search your external component code for these patterns:
# Find .state member accessgrep -r '\.state' --include='*.cpp' --include='*.h'
# Find set_options() callsgrep -r 'set_options' --include='*.cpp' --include='*.h'
# Find vector<string> option storagegrep -r 'vector<.*string>' --include='*.cpp' --include='*.h'Questions?
Section titled “Questions?”If you have questions about these changes or need help migrating your external component, please ask in the ESPHome Discord or open a discussion on GitHub.
Related Documentation
Section titled “Related Documentation”Copyright © 2026 ESPHome - A project from the Open Home Foundation
Comments
Feel free to leave a comment here to discuss this post with others. You can ask questions, share your experience, or suggest improvements. If you have a question about a specific feature or issue, please consider using the ESPHome Discord. Stick to English and follow ESPHome's code of conduct. These comments exist on a discussion on GitHub, so you can also comment there directly if you prefer.