From e8465bfcdafbaf80580f9acd25f5bfe4b07a9bf6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Jan 2026 22:58:49 -1000 Subject: [PATCH 1/8] [select] Return std::string_view from current_option() --- esphome/components/api/api_connection.cpp | 3 ++- esphome/components/ld2410/ld2410.cpp | 7 ++++--- esphome/components/ld2412/ld2412.cpp | 7 ++++--- esphome/components/ld2450/ld2450.cpp | 6 ++++-- esphome/components/mqtt/mqtt_select.cpp | 2 +- esphome/components/prometheus/prometheus_handler.cpp | 2 +- esphome/components/select/select.cpp | 4 +++- esphome/components/select/select.h | 12 ++++++++---- esphome/components/web_server/web_server.cpp | 4 ++-- esphome/components/web_server/web_server.h | 3 ++- 10 files changed, 31 insertions(+), 19 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index fb3548d117a..4291dedc20d 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -914,7 +914,8 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection bool is_single) { auto *select = static_cast(entity); SelectStateResponse resp; - resp.state = StringRef(select->current_option()); + auto state = select->current_option(); + resp.state = StringRef(state.data(), state.size()); resp.missing_state = !select->has_state(); return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index c9b4333f7ef..aead6b822de 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -442,7 +442,8 @@ bool LD2410Component::handle_ack_data_() { ESP_LOGV(TAG, "Baud rate change"); #ifdef USE_SELECT if (this->baud_rate_select_ != nullptr) { - ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option()); + auto baud = this->baud_rate_select_->current_option(); + ESP_LOGE(TAG, "Change baud rate to %.*s and reinstall", (int) baud.size(), baud.data()); } #endif break; @@ -766,10 +767,10 @@ void LD2410Component::set_light_out_control() { #endif #ifdef USE_SELECT if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) { - this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option()); + this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option().data()); } if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->has_state()) { - this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option()); + this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().data()); } #endif this->set_config_mode_(true); diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index 620ac9886bd..859b9ccef53 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -486,7 +486,8 @@ bool LD2412Component::handle_ack_data_() { ESP_LOGV(TAG, "Baud rate change"); #ifdef USE_SELECT if (this->baud_rate_select_ != nullptr) { - ESP_LOGW(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option()); + auto baud = this->baud_rate_select_->current_option(); + ESP_LOGW(TAG, "Change baud rate to %.*s and reinstall", (int) baud.size(), baud.data()); } #endif break; @@ -790,7 +791,7 @@ void LD2412Component::set_basic_config() { 1, TOTAL_GATES, DEFAULT_PRESENCE_TIMEOUT, 0, #endif #ifdef USE_SELECT - find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option()), + find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().data()), #else 0x01, // Default value if not using select #endif @@ -844,7 +845,7 @@ void LD2412Component::set_light_out_control() { #endif #ifdef USE_SELECT if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) { - this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option()); + this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option().data()); } #endif uint8_t value[2] = {this->light_function_, this->light_threshold_}; diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 3b85694bc08..f31b486a759 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -637,7 +637,8 @@ bool LD2450Component::handle_ack_data_() { ESP_LOGV(TAG, "Baud rate change"); #ifdef USE_SELECT if (this->baud_rate_select_ != nullptr) { - ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option()); + auto baud = this->baud_rate_select_->current_option(); + ESP_LOGE(TAG, "Change baud rate to %.*s and reinstall", (int) baud.size(), baud.data()); } #endif break; @@ -718,7 +719,8 @@ bool LD2450Component::handle_ack_data_() { this->publish_zone_type(); #ifdef USE_SELECT if (this->zone_type_select_ != nullptr) { - ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->current_option()); + auto zone = this->zone_type_select_->current_option(); + ESP_LOGV(TAG, "Change zone type to: %.*s", (int) zone.size(), zone.data()); } #endif if (this->buffer_data_[10] == 0x00) { diff --git a/esphome/components/mqtt/mqtt_select.cpp b/esphome/components/mqtt/mqtt_select.cpp index 09d90ed46e6..b8bb5b91262 100644 --- a/esphome/components/mqtt/mqtt_select.cpp +++ b/esphome/components/mqtt/mqtt_select.cpp @@ -43,7 +43,7 @@ void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon } bool MQTTSelectComponent::send_initial_state() { if (this->select_->has_state()) { - return this->publish_state(this->select_->current_option()); + return this->publish_state(std::string(this->select_->current_option())); } else { return true; } diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index 88b357041a2..e29d890d78b 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -709,7 +709,7 @@ void PrometheusHandler::select_row_(AsyncResponseStream *stream, select::Select stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(ESPHOME_F("\",value=\"")); - stream->print(obj->current_option()); + stream->print(obj->current_option().data()); stream->print(ESPHOME_F("\"} ")); stream->print(ESPHOME_F("1.0")); stream->print(ESPHOME_F("\n")); diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 28d7eb07d4f..39d74eab9b7 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -38,7 +38,9 @@ void Select::publish_state(size_t index) { #endif } -const char *Select::current_option() const { return this->has_state() ? this->option_at(this->active_index_) : ""; } +std::string_view Select::current_option() const { + return this->has_state() ? std::string_view(this->option_at(this->active_index_)) : std::string_view(); +} void Select::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 330d18ce6f0..7b44ca952b2 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" @@ -33,8 +35,8 @@ class Select : public EntityBase { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" - /// @deprecated Use current_option() instead. This member will be removed in ESPHome 2026.5.0. - ESPDEPRECATED("Use current_option() instead of .state. Will be removed in 2026.5.0", "2025.11.0") + /// @deprecated Use current_option() instead. This member will be removed in ESPHome 2026.7.0. + ESPDEPRECATED("Use current_option() instead of .state. Will be removed in 2026.7.0", "2026.1.0") std::string state{}; Select() = default; @@ -45,8 +47,10 @@ class Select : public EntityBase { void publish_state(const char *state); void publish_state(size_t index); - /// Return the currently selected option (as const char* from flash). - const char *current_option() const; + /// Return the currently selected option, or empty view if no state. + /// The returned view points to string literals from codegen (static storage). + /// Traits are set once at startup and valid for the lifetime of the program. + std::string_view current_option() const; /// Instantiate a SelectCall object to modify this select component's state. SelectCall make_call() { return SelectCall(this); } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index cab177c182f..27fe47842ea 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1416,11 +1416,11 @@ std::string WebServer::select_all_json_generator(WebServer *web_server, void *so auto *obj = (select::Select *) (source); return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : "", DETAIL_ALL); } -std::string WebServer::select_json_(select::Select *obj, const char *value, JsonDetail start_config) { +std::string WebServer::select_json_(select::Select *obj, std::string_view value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "select", value, value, start_config); + set_json_icon_state_value(root, obj, "select", value.data(), value.data(), start_config); if (start_config == DETAIL_ALL) { JsonArray opt = root[ESPHOME_F("option")].to(); for (auto &option : obj->traits.get_options()) { diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 3e1dd867c64..f698f967d17 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -588,7 +589,7 @@ class WebServer : public Controller, std::string text_json_(text::Text *obj, const std::string &value, JsonDetail start_config); #endif #ifdef USE_SELECT - std::string select_json_(select::Select *obj, const char *value, JsonDetail start_config); + std::string select_json_(select::Select *obj, std::string_view value, JsonDetail start_config); #endif #ifdef USE_CLIMATE std::string climate_json_(climate::Climate *obj, JsonDetail start_config); From 6596186240af34fb085ab1bb70250f3501c29e38 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Jan 2026 23:00:34 -1000 Subject: [PATCH 2/8] actually commit thte tests --- tests/components/template/common-base.yaml | 32 ++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index e050c0b3070..6c3c72e665d 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -243,6 +243,7 @@ number: select: - platform: template + id: template_select name: "Template select" optimistic: true options: @@ -250,6 +251,37 @@ select: - two - three initial_option: two + # Test current_option() returning std::string_view - migration guide examples + on_value: + - lambda: |- + // Migration guide: Check if select has a state + // OLD: if (id(template_select).current_option() != nullptr) + // NEW: Check with .empty() + if (!id(template_select).current_option().empty()) { + ESP_LOGI("test", "Select has state"); + } + + // Migration guide: Compare option values + // OLD: if (strcmp(id(template_select).current_option(), "one") == 0) + // NEW: Direct comparison works safely even when empty + if (id(template_select).current_option() == "one") { + ESP_LOGI("test", "Option is 'one'"); + } + if (id(template_select).current_option() != "two") { + ESP_LOGI("test", "Option is not 'two'"); + } + + // Migration guide: Logging options + // Option 1: Using .data() - relies on null-termination from traits (safe for codegen strings) + ESP_LOGI("test", "Current option (data): %s", id(template_select).current_option().data()); + + // Option 2: Using %.*s format with size - safer, doesn't assume null-termination + auto option = id(template_select).current_option(); + ESP_LOGI("test", "Current option (safe): %.*s", (int) option.size(), option.data()); + + // Migration guide: Store in std::string + std::string stored_option(id(template_select).current_option()); + ESP_LOGI("test", "Stored: %s", stored_option.c_str()); lock: - platform: template From 26671cb1ee311b919db72efacf02d4e91a1fb382 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Jan 2026 23:05:30 -1000 Subject: [PATCH 3/8] [select] Return std::string_view from current_option() --- esphome/components/prometheus/prometheus_handler.cpp | 1 + esphome/components/web_server/web_server.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index e29d890d78b..908829f6634 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -709,6 +709,7 @@ void PrometheusHandler::select_row_(AsyncResponseStream *stream, select::Select stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(ESPHOME_F("\",value=\"")); + // current_option() returns string_view pointing to null-terminated string literals from codegen stream->print(obj->current_option().data()); stream->print(ESPHOME_F("\"} ")); stream->print(ESPHOME_F("1.0")); diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 27fe47842ea..0ae838f2817 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1420,6 +1420,7 @@ std::string WebServer::select_json_(select::Select *obj, std::string_view value, json::JsonBuilder builder; JsonObject root = builder.root(); + // value points to null-terminated string literals from codegen (via current_option()) set_json_icon_state_value(root, obj, "select", value.data(), value.data(), start_config); if (start_config == DETAIL_ALL) { JsonArray opt = root[ESPHOME_F("option")].to(); From 606ce9cfd2dd7690d2eb0be08e8837a283e80ae9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Jan 2026 16:12:12 -1000 Subject: [PATCH 4/8] stringref --- esphome/components/api/api_connection.cpp | 3 +-- esphome/components/display_menu_base/menu_item.cpp | 3 ++- esphome/components/ld2410/ld2410.cpp | 6 +++--- esphome/components/ld2412/ld2412.cpp | 6 +++--- esphome/components/ld2450/ld2450.cpp | 4 ++-- esphome/components/mqtt/mqtt_select.cpp | 3 ++- esphome/components/prometheus/prometheus_handler.cpp | 4 ++-- esphome/components/select/select.cpp | 4 ++-- esphome/components/select/select.h | 9 ++++----- esphome/components/web_server/web_server.cpp | 4 ++-- esphome/components/web_server/web_server.h | 2 +- 11 files changed, 24 insertions(+), 24 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4291dedc20d..954acc87d9d 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -914,8 +914,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection bool is_single) { auto *select = static_cast(entity); SelectStateResponse resp; - auto state = select->current_option(); - resp.state = StringRef(state.data(), state.size()); + resp.state = select->current_option(); resp.missing_state = !select->has_state(); return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } diff --git a/esphome/components/display_menu_base/menu_item.cpp b/esphome/components/display_menu_base/menu_item.cpp index 08f758045e5..ad8b03de606 100644 --- a/esphome/components/display_menu_base/menu_item.cpp +++ b/esphome/components/display_menu_base/menu_item.cpp @@ -42,7 +42,8 @@ std::string MenuItemSelect::get_value_text() const { result = this->value_getter_.value()(this); } else { if (this->select_var_ != nullptr) { - result = this->select_var_->current_option(); + auto option = this->select_var_->current_option(); + result.assign(option.c_str(), option.size()); } } diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index aead6b822de..5294f7cd36b 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -443,7 +443,7 @@ bool LD2410Component::handle_ack_data_() { #ifdef USE_SELECT if (this->baud_rate_select_ != nullptr) { auto baud = this->baud_rate_select_->current_option(); - ESP_LOGE(TAG, "Change baud rate to %.*s and reinstall", (int) baud.size(), baud.data()); + ESP_LOGE(TAG, "Change baud rate to %.*s and reinstall", (int) baud.size(), baud.c_str()); } #endif break; @@ -767,10 +767,10 @@ void LD2410Component::set_light_out_control() { #endif #ifdef USE_SELECT if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) { - this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option().data()); + this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option().c_str()); } if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->has_state()) { - this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().data()); + this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str()); } #endif this->set_config_mode_(true); diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index 859b9ccef53..c2f441e472c 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -487,7 +487,7 @@ bool LD2412Component::handle_ack_data_() { #ifdef USE_SELECT if (this->baud_rate_select_ != nullptr) { auto baud = this->baud_rate_select_->current_option(); - ESP_LOGW(TAG, "Change baud rate to %.*s and reinstall", (int) baud.size(), baud.data()); + ESP_LOGW(TAG, "Change baud rate to %.*s and reinstall", (int) baud.size(), baud.c_str()); } #endif break; @@ -791,7 +791,7 @@ void LD2412Component::set_basic_config() { 1, TOTAL_GATES, DEFAULT_PRESENCE_TIMEOUT, 0, #endif #ifdef USE_SELECT - find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().data()), + find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str()), #else 0x01, // Default value if not using select #endif @@ -845,7 +845,7 @@ void LD2412Component::set_light_out_control() { #endif #ifdef USE_SELECT if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) { - this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option().data()); + this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option().c_str()); } #endif uint8_t value[2] = {this->light_function_, this->light_threshold_}; diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index f31b486a759..58d469b2a79 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -638,7 +638,7 @@ bool LD2450Component::handle_ack_data_() { #ifdef USE_SELECT if (this->baud_rate_select_ != nullptr) { auto baud = this->baud_rate_select_->current_option(); - ESP_LOGE(TAG, "Change baud rate to %.*s and reinstall", (int) baud.size(), baud.data()); + ESP_LOGE(TAG, "Change baud rate to %.*s and reinstall", (int) baud.size(), baud.c_str()); } #endif break; @@ -720,7 +720,7 @@ bool LD2450Component::handle_ack_data_() { #ifdef USE_SELECT if (this->zone_type_select_ != nullptr) { auto zone = this->zone_type_select_->current_option(); - ESP_LOGV(TAG, "Change zone type to: %.*s", (int) zone.size(), zone.data()); + ESP_LOGV(TAG, "Change zone type to: %.*s", (int) zone.size(), zone.c_str()); } #endif if (this->buffer_data_[10] == 0x00) { diff --git a/esphome/components/mqtt/mqtt_select.cpp b/esphome/components/mqtt/mqtt_select.cpp index b8bb5b91262..03ab82312b2 100644 --- a/esphome/components/mqtt/mqtt_select.cpp +++ b/esphome/components/mqtt/mqtt_select.cpp @@ -43,7 +43,8 @@ void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon } bool MQTTSelectComponent::send_initial_state() { if (this->select_->has_state()) { - return this->publish_state(std::string(this->select_->current_option())); + auto option = this->select_->current_option(); + return this->publish_state(std::string(option.c_str(), option.size())); } else { return true; } diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index 908829f6634..6a50260d942 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -709,8 +709,8 @@ void PrometheusHandler::select_row_(AsyncResponseStream *stream, select::Select stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(ESPHOME_F("\",value=\"")); - // current_option() returns string_view pointing to null-terminated string literals from codegen - stream->print(obj->current_option().data()); + auto option = obj->current_option(); + stream->write(option.c_str(), option.size()); stream->print(ESPHOME_F("\"} ")); stream->print(ESPHOME_F("1.0")); stream->print(ESPHOME_F("\n")); diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 39d74eab9b7..3d70e94d473 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -38,8 +38,8 @@ void Select::publish_state(size_t index) { #endif } -std::string_view Select::current_option() const { - return this->has_state() ? std::string_view(this->option_at(this->active_index_)) : std::string_view(); +StringRef Select::current_option() const { + return this->has_state() ? StringRef(this->option_at(this->active_index_)) : StringRef(); } void Select::add_on_state_callback(std::function &&callback) { diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 7b44ca952b2..8b054877043 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -1,10 +1,9 @@ #pragma once -#include - #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" #include "select_call.h" #include "select_traits.h" @@ -47,10 +46,10 @@ class Select : public EntityBase { void publish_state(const char *state); void publish_state(size_t index); - /// Return the currently selected option, or empty view if no state. - /// The returned view points to string literals from codegen (static storage). + /// Return the currently selected option, or empty StringRef if no state. + /// The returned StringRef points to string literals from codegen (static storage). /// Traits are set once at startup and valid for the lifetime of the program. - std::string_view current_option() const; + StringRef current_option() const; /// Instantiate a SelectCall object to modify this select component's state. SelectCall make_call() { return SelectCall(this); } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index bfecf0bf278..15261f387e2 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1420,12 +1420,12 @@ std::string WebServer::select_all_json_generator(WebServer *web_server, void *so auto *obj = (select::Select *) (source); return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : "", DETAIL_ALL); } -std::string WebServer::select_json_(select::Select *obj, std::string_view value, JsonDetail start_config) { +std::string WebServer::select_json_(select::Select *obj, StringRef value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); // value points to null-terminated string literals from codegen (via current_option()) - set_json_icon_state_value(root, obj, "select", value.data(), value.data(), start_config); + set_json_icon_state_value(root, obj, "select", value.c_str(), value.c_str(), start_config); if (start_config == DETAIL_ALL) { JsonArray opt = root[ESPHOME_F("option")].to(); for (auto &option : obj->traits.get_options()) { diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 0ad81b5dcac..ff50c1809fd 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -628,7 +628,7 @@ class WebServer : public Controller, std::string text_json_(text::Text *obj, const std::string &value, JsonDetail start_config); #endif #ifdef USE_SELECT - std::string select_json_(select::Select *obj, std::string_view value, JsonDetail start_config); + std::string select_json_(select::Select *obj, StringRef value, JsonDetail start_config); #endif #ifdef USE_CLIMATE std::string climate_json_(climate::Climate *obj, JsonDetail start_config); From 33d2140f1ccf82b12d1cb4a9c0b3f2e1c5b31be1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Jan 2026 16:13:17 -1000 Subject: [PATCH 5/8] stringref --- esphome/components/web_server/web_server.h | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index ff50c1809fd..b62686f0aaf 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -15,7 +15,6 @@ #include #include #include -#include #include #include From 15734c63a14bbba3816d01b4abee9013e1de8900 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Jan 2026 16:18:35 -1000 Subject: [PATCH 6/8] back to print --- esphome/components/prometheus/prometheus_handler.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index 6a50260d942..75910fa73dd 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -709,8 +709,8 @@ void PrometheusHandler::select_row_(AsyncResponseStream *stream, select::Select stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(ESPHOME_F("\",value=\"")); - auto option = obj->current_option(); - stream->write(option.c_str(), option.size()); + // c_str() is safe as option values are null-terminated strings from codegen + stream->print(obj->current_option().c_str()); stream->print(ESPHOME_F("\"} ")); stream->print(ESPHOME_F("1.0")); stream->print(ESPHOME_F("\n")); From cacbb017c0c50bd0790f026290e34e7f9c720cd8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Jan 2026 16:35:04 -1000 Subject: [PATCH 7/8] fix --- esphome/components/web_server/web_server.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 15261f387e2..41d225c0d82 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1393,7 +1393,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->select_json_(obj, obj->has_state() ? obj->current_option() : "", detail); + std::string data = this->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), detail); request->send(200, "application/json", data.c_str()); return; } @@ -1414,11 +1414,11 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM } std::string WebServer::select_state_json_generator(WebServer *web_server, void *source) { auto *obj = (select::Select *) (source); - return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : "", DETAIL_STATE); + return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), DETAIL_STATE); } std::string WebServer::select_all_json_generator(WebServer *web_server, void *source) { auto *obj = (select::Select *) (source); - return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : "", DETAIL_ALL); + return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), DETAIL_ALL); } std::string WebServer::select_json_(select::Select *obj, StringRef value, JsonDetail start_config) { json::JsonBuilder builder; From 54668648dffa436e550cdfbbc670a20b72b1b6ea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Jan 2026 16:56:02 -1000 Subject: [PATCH 8/8] fix --- tests/components/template/common-base.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index 6c3c72e665d..134ad4d046b 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -272,12 +272,12 @@ select: } // Migration guide: Logging options - // Option 1: Using .data() - relies on null-termination from traits (safe for codegen strings) - ESP_LOGI("test", "Current option (data): %s", id(template_select).current_option().data()); + // Option 1: Using .c_str() - StringRef guarantees null-termination + ESP_LOGI("test", "Current option: %s", id(template_select).current_option().c_str()); - // Option 2: Using %.*s format with size - safer, doesn't assume null-termination + // Option 2: Using %.*s format with size auto option = id(template_select).current_option(); - ESP_LOGI("test", "Current option (safe): %.*s", (int) option.size(), option.data()); + ESP_LOGI("test", "Current option (safe): %.*s", (int) option.size(), option.c_str()); // Migration guide: Store in std::string std::string stored_option(id(template_select).current_option());