diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 382c4acc16..69c7efea32 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -877,7 +877,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection bool is_single) { auto *select = static_cast(entity); SelectStateResponse resp; - resp.set_state(StringRef(select->state)); + resp.set_state(StringRef(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/logger/select/logger_level_select.cpp b/esphome/components/logger/select/logger_level_select.cpp index 6d60a3ae47..5537df570c 100644 --- a/esphome/components/logger/select/logger_level_select.cpp +++ b/esphome/components/logger/select/logger_level_select.cpp @@ -3,10 +3,10 @@ namespace esphome::logger { void LoggerLevelSelect::publish_state(int level) { - const auto &option = this->at(level_to_index(level)); - if (!option) + auto index = level_to_index(level); + if (!this->has_index(index)) return; - Select::publish_state(option.value()); + Select::publish_state(index); } void LoggerLevelSelect::setup() { diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 5e30be3c13..b52504ea28 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -7,24 +7,39 @@ namespace select { static const char *const TAG = "select"; -void Select::publish_state(const std::string &state) { +void Select::publish_state(const std::string &state) { this->publish_state(state.c_str()); } + +void Select::publish_state(const char *state) { auto index = this->index_of(state); - const auto *name = this->get_name().c_str(); if (index.has_value()) { - this->set_has_state(true); - this->state = state; - ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", name, state.c_str(), index.value()); - this->state_callback_.call(state, index.value()); + this->publish_state(index.value()); } else { - ESP_LOGE(TAG, "'%s': invalid state for publish_state(): %s", name, state.c_str()); + ESP_LOGE(TAG, "'%s': invalid state for publish_state(): %s", this->get_name().c_str(), state); } } +void Select::publish_state(size_t index) { + if (!this->has_index(index)) { + ESP_LOGE(TAG, "'%s': invalid index for publish_state(): %zu", this->get_name().c_str(), index); + return; + } + const char *option = this->option_at(index); + this->set_has_state(true); + this->active_index_ = index; + ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", this->get_name().c_str(), option, index); + // Callback signature requires std::string, create temporary for compatibility + this->state_callback_.call(std::string(option), index); +} + +const char *Select::current_option() const { return this->option_at(this->active_index_); } + void Select::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } -bool Select::has_option(const std::string &option) const { return this->index_of(option).has_value(); } +bool Select::has_option(const std::string &option) const { return this->index_of(option.c_str()).has_value(); } + +bool Select::has_option(const char *option) const { return this->index_of(option).has_value(); } bool Select::has_index(size_t index) const { return index < this->size(); } @@ -33,10 +48,12 @@ size_t Select::size() const { return options.size(); } -optional Select::index_of(const std::string &option) const { +optional Select::index_of(const std::string &option) const { return this->index_of(option.c_str()); } + +optional Select::index_of(const char *option) const { const auto &options = traits.get_options(); for (size_t i = 0; i < options.size(); i++) { - if (strcmp(options[i], option.c_str()) == 0) { + if (strcmp(options[i], option) == 0) { return i; } } @@ -45,7 +62,7 @@ optional Select::index_of(const std::string &option) const { optional Select::active_index() const { if (this->has_state()) { - return this->index_of(this->state); + return this->active_index_; } else { return {}; } diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index eabb39898b..81c8f68362 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -30,16 +30,21 @@ namespace select { */ class Select : public EntityBase { public: - std::string state; SelectTraits traits; void publish_state(const std::string &state); + 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; /// Instantiate a SelectCall object to modify this select component's state. SelectCall make_call() { return SelectCall(this); } /// Return whether this select component contains the provided option. bool has_option(const std::string &option) const; + bool has_option(const char *option) const; /// Return whether this select component contains the provided index offset. bool has_index(size_t index) const; @@ -49,6 +54,7 @@ class Select : public EntityBase { /// Find the (optional) index offset of the provided option value. optional index_of(const std::string &option) const; + optional index_of(const char *option) const; /// Return the (optional) index offset of the currently active option. optional active_index() const; @@ -64,6 +70,18 @@ class Select : public EntityBase { protected: friend class SelectCall; + size_t active_index_{0}; + + /** Set the value of the select by index, this is an optional virtual method. + * + * This method is called by the SelectCall when the index is already known. + * Default implementation converts to string and calls control(). + * Override this to work directly with indices and avoid string conversions. + * + * @param index The index as validated by the SelectCall. + */ + virtual void control(size_t index) { this->control(this->option_at(index)); } + /** Set the value of the select, this is a virtual method that each select integration must implement. * * This method is called by the SelectCall. diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index dd398b4052..144090e21f 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -11,6 +11,8 @@ SelectCall &SelectCall::set_option(const std::string &option) { return with_operation(SELECT_OP_SET).with_option(option); } +SelectCall &SelectCall::set_option(const char *option) { return with_operation(SELECT_OP_SET).with_option(option); } + SelectCall &SelectCall::set_index(size_t index) { return with_operation(SELECT_OP_SET_INDEX).with_index(index); } SelectCall &SelectCall::select_next(bool cycle) { return with_operation(SELECT_OP_NEXT).with_cycle(cycle); } @@ -31,8 +33,11 @@ SelectCall &SelectCall::with_cycle(bool cycle) { return *this; } -SelectCall &SelectCall::with_option(const std::string &option) { - this->option_ = option; +SelectCall &SelectCall::with_option(const std::string &option) { return this->with_option(option.c_str()); } + +SelectCall &SelectCall::with_option(const char *option) { + // Find the option index - this validates the option exists + this->index_ = this->parent_->index_of(option); return *this; } @@ -56,64 +61,52 @@ void SelectCall::perform() { return; } - std::string target_value; + size_t target_index; - if (this->operation_ == SELECT_OP_SET) { - ESP_LOGD(TAG, "'%s' - Setting", name); - if (!this->option_.has_value()) { - ESP_LOGW(TAG, "'%s' - No option value set for SelectCall", name); - return; + if (this->operation_ == SELECT_OP_SET || this->operation_ == SELECT_OP_SET_INDEX) { + if (this->operation_ == SELECT_OP_SET) { + ESP_LOGD(TAG, "'%s' - Setting", name); } - target_value = this->option_.value(); - } else if (this->operation_ == SELECT_OP_SET_INDEX) { if (!this->index_.has_value()) { - ESP_LOGW(TAG, "'%s' - No index value set for SelectCall", name); + ESP_LOGW(TAG, "'%s' - No option value set for SelectCall", name); return; } if (this->index_.value() >= options.size()) { ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", name, this->index_.value()); return; } - target_value = options[this->index_.value()]; + target_index = this->index_.value(); } else if (this->operation_ == SELECT_OP_FIRST) { - target_value = options.front(); + target_index = 0; } else if (this->operation_ == SELECT_OP_LAST) { - target_value = options.back(); + target_index = options.size() - 1; } else if (this->operation_ == SELECT_OP_NEXT || this->operation_ == SELECT_OP_PREVIOUS) { auto cycle = this->cycle_; ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, this->operation_ == SELECT_OP_NEXT ? "next" : "previous", cycle ? "" : "out"); if (!parent->has_state()) { - target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back(); + target_index = this->operation_ == SELECT_OP_NEXT ? 0 : options.size() - 1; } else { - auto index = parent->index_of(parent->state); - if (index.has_value()) { - auto size = options.size(); - if (cycle) { - auto use_index = (size + index.value() + (this->operation_ == SELECT_OP_NEXT ? +1 : -1)) % size; - target_value = options[use_index]; - } else { - if (this->operation_ == SELECT_OP_PREVIOUS && index.value() > 0) { - target_value = options[index.value() - 1]; - } else if (this->operation_ == SELECT_OP_NEXT && index.value() < options.size() - 1) { - target_value = options[index.value() + 1]; - } else { - return; - } - } + // Use cached active_index_ instead of index_of() lookup + auto index = parent->active_index_; + auto size = options.size(); + if (cycle) { + target_index = (size + index + (this->operation_ == SELECT_OP_NEXT ? +1 : -1)) % size; } else { - target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back(); + if (this->operation_ == SELECT_OP_PREVIOUS && index > 0) { + target_index = index - 1; + } else if (this->operation_ == SELECT_OP_NEXT && index < options.size() - 1) { + target_index = index + 1; + } else { + return; + } } } } - if (!parent->has_option(target_value)) { - ESP_LOGW(TAG, "'%s' - Option %s is not a valid option", name, target_value.c_str()); - return; - } - - ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, target_value.c_str()); - parent->control(target_value); + // All operations use indices, call control() by index to avoid string conversion + ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, options[target_index]); + parent->control(target_index); } } // namespace select diff --git a/esphome/components/select/select_call.h b/esphome/components/select/select_call.h index efc9a982ec..a0c63a0e39 100644 --- a/esphome/components/select/select_call.h +++ b/esphome/components/select/select_call.h @@ -23,6 +23,7 @@ class SelectCall { void perform(); SelectCall &set_option(const std::string &option); + SelectCall &set_option(const char *option); SelectCall &set_index(size_t index); SelectCall &select_next(bool cycle); @@ -33,11 +34,11 @@ class SelectCall { SelectCall &with_operation(SelectOperation operation); SelectCall &with_cycle(bool cycle); SelectCall &with_option(const std::string &option); + SelectCall &with_option(const char *option); SelectCall &with_index(size_t index); protected: Select *const parent_; - optional option_; optional index_; SelectOperation operation_{SELECT_OP_NONE}; bool cycle_; diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 3ea34c3c7c..03ef1d482b 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -24,7 +24,7 @@ void TemplateSelect::setup() { ESP_LOGD(TAG, "State from initial: %s", this->option_at(index)); } - this->publish_state(this->at(index).value()); + this->publish_state(index); } void TemplateSelect::update() { diff --git a/esphome/components/tuya/select/tuya_select.cpp b/esphome/components/tuya/select/tuya_select.cpp index 7c1cd09d06..07e3ce44ee 100644 --- a/esphome/components/tuya/select/tuya_select.cpp +++ b/esphome/components/tuya/select/tuya_select.cpp @@ -17,8 +17,7 @@ void TuyaSelect::setup() { return; } size_t mapping_idx = std::distance(mappings.cbegin(), it); - auto value = this->at(mapping_idx); - this->publish_state(value.value()); + this->publish_state(mapping_idx); }); }