|
| 1 | +--- |
| 2 | +date: 2025-11-07 |
| 3 | +authors: |
| 4 | + - bdraco |
| 5 | +comments: true |
| 6 | +--- |
| 7 | + |
| 8 | +# Select Entity Class: Index-Based Operations and Flash Storage |
| 9 | + |
| 10 | +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. |
| 11 | + |
| 12 | +<!-- more --> |
| 13 | + |
| 14 | +## Background |
| 15 | + |
| 16 | +Two related PRs optimize the Select entity class: |
| 17 | + |
| 18 | +**[PR #11623](https://github.com/esphome/esphome/pull/11623): Index-Based Operations** |
| 19 | +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). |
| 20 | + |
| 21 | +**[PR #11514](https://github.com/esphome/esphome/pull/11514): Store Options in Flash** |
| 22 | +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. |
| 23 | + |
| 24 | +## What's Changing |
| 25 | + |
| 26 | +### For ESPHome 2025.11.0 and Later |
| 27 | + |
| 28 | +**Storage Changes (Breaking - [PR #11514](https://github.com/esphome/esphome/pull/11514)):** |
| 29 | +```cpp |
| 30 | +// OLD - heap-allocated strings |
| 31 | +std::vector<std::string> options; |
| 32 | +traits.set_options(options); |
| 33 | + |
| 34 | +// NEW - flash-stored string literals |
| 35 | +traits.set_options({"Option 1", "Option 2", "Option 3"}); |
| 36 | +``` |
| 37 | +
|
| 38 | +**State Access Changes (Deprecation - [PR #11623](https://github.com/esphome/esphome/pull/11623)):** |
| 39 | +```cpp |
| 40 | +// OLD - deprecated, shows warnings (works until 2026.5.0) |
| 41 | +std::string current = my_select->state; |
| 42 | +
|
| 43 | +// NEW - required after 2026.5.0 |
| 44 | +const char *current = my_select->current_option(); |
| 45 | +``` |
| 46 | + |
| 47 | +## Who This Affects |
| 48 | + |
| 49 | +This affects **external components** that: |
| 50 | + |
| 51 | +- Manually call `set_options()` on SelectTraits in C++ code (Python code generation already uses the correct syntax) |
| 52 | +- Access the `.state` member of Select objects |
| 53 | +- Iterate over or compare select options |
| 54 | + |
| 55 | +**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++. |
| 56 | + |
| 57 | +## Migration Guide |
| 58 | + |
| 59 | +### 1. Setting Options (Required Now) |
| 60 | + |
| 61 | +**In setup() methods:** |
| 62 | +```cpp |
| 63 | +// OLD |
| 64 | +std::vector<std::string> options = {"Low", "Medium", "High"}; |
| 65 | +this->traits.set_options(options); |
| 66 | + |
| 67 | +// NEW - use initializer list with string literals |
| 68 | +this->traits.set_options({"Low", "Medium", "High"}); |
| 69 | +``` |
| 70 | +
|
| 71 | +**For runtime-determined options** (rare), you must store the strings persistently: |
| 72 | +```cpp |
| 73 | +#include "esphome/core/helpers.h" // For FixedVector |
| 74 | +
|
| 75 | +class MySelect : public select::Select { |
| 76 | + protected: |
| 77 | + // Storage for actual string data (must persist for lifetime) |
| 78 | + std::vector<std::string> stored_options_; |
| 79 | + // Pointers into stored_options_ |
| 80 | + FixedVector<const char*> option_ptrs_; |
| 81 | +
|
| 82 | + void setup() override { |
| 83 | + // Read dynamic options from device/config (truly runtime-determined) |
| 84 | + uint8_t mode_count = this->read_mode_count_from_device(); |
| 85 | + this->stored_options_.resize(mode_count); |
| 86 | + for (uint8_t i = 0; i < mode_count; i++) { |
| 87 | + this->stored_options_[i] = this->read_mode_name_from_device(i); |
| 88 | + } |
| 89 | +
|
| 90 | + // Build pointer array pointing into stored_options_ |
| 91 | + this->option_ptrs_.init(this->stored_options_.size()); |
| 92 | + for (const auto &opt : this->stored_options_) { |
| 93 | + this->option_ptrs_.push_back(opt.c_str()); |
| 94 | + } |
| 95 | +
|
| 96 | + // Set the traits (pointers remain valid because stored_options_ persists) |
| 97 | + this->traits.set_options(this->option_ptrs_); |
| 98 | + } |
| 99 | +}; |
| 100 | +``` |
| 101 | + |
| 102 | +### 2. Accessing Options (Required Now) |
| 103 | + |
| 104 | +**Reading the options list:** |
| 105 | +```cpp |
| 106 | +// OLD - copying (deleted copy constructor) |
| 107 | +auto options = traits.get_options(); |
| 108 | + |
| 109 | +// NEW - use const reference |
| 110 | +const auto &options = traits.get_options(); |
| 111 | + |
| 112 | +// Individual options are now const char* |
| 113 | +const char *option = options[0]; // Not std::string |
| 114 | + |
| 115 | +// If you need std::string: |
| 116 | +std::string str = std::string(options[0]); |
| 117 | +``` |
| 118 | + |
| 119 | +### 3. Reading Current Selection (Deprecated, Remove by 2026.5.0) |
| 120 | + |
| 121 | +**In YAML lambdas:** |
| 122 | +```yaml |
| 123 | +# OLD - shows deprecation warning (works until 2026.5.0) |
| 124 | +lambda: 'return id(my_select).state == "option1";' |
| 125 | + |
| 126 | +# NEW - required after 2026.5.0, use strcmp() |
| 127 | +lambda: 'return strcmp(id(my_select).current_option(), "option1") == 0;' |
| 128 | + |
| 129 | +# Or convert to std::string if you prefer == operator (less efficient) |
| 130 | +lambda: 'return std::string(id(my_select).current_option()) == "option1";' |
| 131 | +``` |
| 132 | +
|
| 133 | +**In C++ code:** |
| 134 | +```cpp |
| 135 | +// OLD - deprecated (works until 2026.5.0) |
| 136 | +std::string current = my_select->state; |
| 137 | +ESP_LOGD(TAG, "Current: %s", my_select->state.c_str()); |
| 138 | + |
| 139 | +// NEW - required after 2026.5.0 |
| 140 | +const char *current = my_select->current_option(); |
| 141 | +ESP_LOGD(TAG, "Current: %s", current); |
| 142 | + |
| 143 | +// If you need std::string: |
| 144 | +std::string current = my_select->current_option(); // Implicit conversion |
| 145 | +``` |
| 146 | + |
| 147 | +### 4. Publishing State (New Methods Available) |
| 148 | + |
| 149 | +**Prefer index-based operations:** |
| 150 | +```cpp |
| 151 | +// OLD - string-based (still works but less efficient) |
| 152 | +this->publish_state("option1"); |
| 153 | + |
| 154 | +// NEW - index-based (more efficient) |
| 155 | +this->publish_state(0); // Publish by index |
| 156 | +``` |
| 157 | +
|
| 158 | +### 5. String Comparisons |
| 159 | +
|
| 160 | +**When comparing options:** |
| 161 | +```cpp |
| 162 | +// OLD - std::string comparison |
| 163 | +if (options[i] == "value") { } |
| 164 | +
|
| 165 | +// NEW - use strcmp() |
| 166 | +if (strcmp(options[i], "value") == 0) { } |
| 167 | +
|
| 168 | +// BETTER - use Select helper methods |
| 169 | +auto idx = this->index_of(value); |
| 170 | +if (idx.has_value()) { |
| 171 | + this->publish_state(idx.value()); |
| 172 | +} |
| 173 | +``` |
| 174 | + |
| 175 | +### 6. Overriding control() Method (Required) |
| 176 | + |
| 177 | +**IMPORTANT:** You **must** override at least one `control()` method. If you override neither, they will call each other infinitely. |
| 178 | + |
| 179 | +```cpp |
| 180 | +class MySelect : public select::Select { |
| 181 | + protected: |
| 182 | + // Option 1: String-based control (still works, but less efficient) |
| 183 | + void control(const std::string &value) override { |
| 184 | + // This version receives the string value |
| 185 | + auto idx = this->index_of(value); // strcmp lookup needed |
| 186 | + if (idx.has_value()) { |
| 187 | + this->send_to_device(idx.value()); |
| 188 | + } |
| 189 | + } |
| 190 | + |
| 191 | + // Option 2: Index-based control (preferred, more efficient) |
| 192 | + void control(size_t index) override { |
| 193 | + // This version receives the index directly |
| 194 | + this->send_to_device(index); // No lookup needed |
| 195 | + } |
| 196 | +}; |
| 197 | +``` |
| 198 | +
|
| 199 | +**Which to override?** |
| 200 | +- Override `control(size_t index)` (preferred) - avoids string conversions and lookups |
| 201 | +- Override `control(const std::string &value)` - if you need the actual string value |
| 202 | +- Override both (rare) - if you need different handling for each case |
| 203 | +
|
| 204 | +## Supporting Multiple ESPHome Versions |
| 205 | +
|
| 206 | +### .state Member Access (Deprecated but Still Exists) |
| 207 | +
|
| 208 | +The `.state` member still exists until 2026.5.0, so you can use version guards: |
| 209 | +
|
| 210 | +```cpp |
| 211 | +#if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0) |
| 212 | + const char *current = my_select->current_option(); |
| 213 | +#else |
| 214 | + const char *current = my_select->state.c_str(); |
| 215 | +#endif |
| 216 | +``` |
| 217 | + |
| 218 | +### Options Storage (Hard Breaking Change) |
| 219 | + |
| 220 | +The old `set_options(std::vector<std::string>)` API was completely removed in [PR #11514](https://github.com/esphome/esphome/pull/11514). Version guards are **not possible** because the old API no longer exists. |
| 221 | + |
| 222 | +External components must either: |
| 223 | +- Update to the new API to support ESPHome 2025.11.0+ |
| 224 | +- Pin to ESPHome versions before 2025.11.0 if they can't update yet |
| 225 | + |
| 226 | +There is no way to support both old and new ESPHome versions for options storage without maintaining separate branches. |
| 227 | + |
| 228 | +## Timeline |
| 229 | + |
| 230 | +- **ESPHome 2025.11.0 (November 2025):** |
| 231 | + - Options storage change is active (breaking change) |
| 232 | + - `.state` member deprecated but still works with warnings |
| 233 | + - New `current_option()` method available |
| 234 | + |
| 235 | +- **ESPHome 2026.5.0 (May 2026):** |
| 236 | + - `.state` member will be removed |
| 237 | + - Must use `current_option()` method |
| 238 | + |
| 239 | +## Finding Code That Needs Updates |
| 240 | + |
| 241 | +Search your external component code for these patterns: |
| 242 | + |
| 243 | +```bash |
| 244 | +# Find .state member access |
| 245 | +grep -r '\.state' --include='*.cpp' --include='*.h' |
| 246 | + |
| 247 | +# Find set_options() calls |
| 248 | +grep -r 'set_options' --include='*.cpp' --include='*.h' |
| 249 | + |
| 250 | +# Find vector<string> option storage |
| 251 | +grep -r 'vector<.*string>' --include='*.cpp' --include='*.h' |
| 252 | +``` |
| 253 | + |
| 254 | +## Questions? |
| 255 | + |
| 256 | +If you have questions about these changes or need help migrating your external component, please ask in the [ESPHome Discord](https://discord.gg/KhAMKrd) or open a [discussion on GitHub](https://github.com/esphome/esphome/discussions). |
| 257 | + |
| 258 | +## Related Documentation |
| 259 | + |
| 260 | +- [Select Component Documentation](https://esphome.io/components/select/index.html) |
| 261 | +- [PR #11623: Index-Based Operations](https://github.com/esphome/esphome/pull/11623) |
| 262 | +- [PR #11514: Store Options in Flash](https://github.com/esphome/esphome/pull/11514) |
0 commit comments