From 5a2774876ab32572e335accfdcde02189885829d Mon Sep 17 00:00:00 2001 From: clydebarrow <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 3 Feb 2026 13:14:09 +1100 Subject: [PATCH 1/3] Use templates to customise classes --- .../components/template/select/__init__.py | 76 ++++++++++------- .../template/select/template_select.cpp | 69 +++------------- .../template/select/template_select.h | 82 ++++++++++++++----- 3 files changed, 120 insertions(+), 107 deletions(-) diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py index da57e94b07..566fb2996a 100644 --- a/esphome/components/template/select/__init__.py +++ b/esphome/components/template/select/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_RESTORE_VALUE, CONF_SET_ACTION, ) +from esphome.cpp_generator import TemplateArguments from .. import template_ns @@ -23,29 +24,47 @@ TemplateSelectWithSetAction = template_ns.class_( def validate(config): + errors = [] if CONF_LAMBDA in config: if config[CONF_OPTIMISTIC]: - raise cv.Invalid("optimistic cannot be used with lambda") + errors.append( + cv.Invalid( + "optimistic cannot be used with lambda", path=[CONF_OPTIMISTIC] + ) + ) if CONF_INITIAL_OPTION in config: - raise cv.Invalid("initial_value cannot be used with lambda") + errors.append( + cv.Invalid( + "initial_value cannot be used with lambda", + path=[CONF_INITIAL_OPTION], + ) + ) if CONF_RESTORE_VALUE in config: - raise cv.Invalid("restore_value cannot be used with lambda") + errors.append( + cv.Invalid( + "restore_value cannot be used with lambda", + path=[CONF_RESTORE_VALUE], + ) + ) elif CONF_INITIAL_OPTION in config: if config[CONF_INITIAL_OPTION] not in config[CONF_OPTIONS]: - raise cv.Invalid( - f"initial_option '{config[CONF_INITIAL_OPTION]}' is not a valid option [{', '.join(config[CONF_OPTIONS])}]" + errors.append( + cv.Invalid( + f"initial_option '{config[CONF_INITIAL_OPTION]}' is not a valid option [{', '.join(config[CONF_OPTIONS])}]", + path=[CONF_INITIAL_OPTION], + ) ) else: config[CONF_INITIAL_OPTION] = config[CONF_OPTIONS][0] if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config: - raise cv.Invalid( - "Either optimistic mode must be enabled, or set_action must be set, to handle the option being set." + errors.append( + cv.Invalid( + "Either optimistic mode must be enabled, or set_action must be set, to handle the option being set." + ) ) - - # Use subclass with trigger only when set_action is configured - if CONF_SET_ACTION in config: - config[CONF_ID].type = TemplateSelectWithSetAction + if errors: + raise cv.MultipleInvalid(errors) return config @@ -70,29 +89,28 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var_id = config[CONF_ID] + if CONF_SET_ACTION in config: + var_id.type = TemplateSelectWithSetAction + has_lambda = CONF_LAMBDA in config + optimistic = config.get(CONF_OPTIMISTIC, False) + restore_value = config.get(CONF_RESTORE_VALUE, False) + options = config[CONF_OPTIONS] + initial_option = config.get(CONF_INITIAL_OPTION, 0) + initial_option_index = options.index(initial_option) if not has_lambda else 0 + + var = cg.new_Pvariable( + var_id, + TemplateArguments(has_lambda, optimistic, restore_value, initial_option_index), + ) await cg.register_component(var, config) - await select.register_select(var, config, options=config[CONF_OPTIONS]) + await select.register_select(var, config, options=options) if CONF_LAMBDA in config: - template_ = await cg.process_lambda( + lambda_ = await cg.process_lambda( config[CONF_LAMBDA], [], return_type=cg.optional.template(cg.std_string) ) - cg.add(var.set_template(template_)) - - else: - # Only set if non-default to avoid bloating setup() function - if config[CONF_OPTIMISTIC]: - cg.add(var.set_optimistic(True)) - initial_option_index = config[CONF_OPTIONS].index(config[CONF_INITIAL_OPTION]) - # Only set if non-zero to avoid bloating setup() function - # (initial_option_index_ is zero-initialized in the header) - if initial_option_index != 0: - cg.add(var.set_initial_option_index(initial_option_index)) - - # Only set if True (default is False) - if config.get(CONF_RESTORE_VALUE): - cg.add(var.set_restore_value(True)) + cg.add(var.set_lambda(lambda_)) if CONF_SET_ACTION in config: await automation.build_automation( diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 0849e1a434..5a7b6400c6 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -3,66 +3,17 @@ namespace esphome::template_ { -static const char *const TAG = "template.select"; - -void TemplateSelect::setup() { - if (this->f_.has_value()) - return; - - size_t index = this->initial_option_index_; - if (this->restore_value_) { - this->pref_ = this->make_entity_preference(); - size_t restored_index; - if (this->pref_.load(&restored_index) && this->has_index(restored_index)) { - index = restored_index; - ESP_LOGD(TAG, "State from restore: %s", this->option_at(index)); - } else { - ESP_LOGD(TAG, "State from initial (could not load or invalid stored index): %s", this->option_at(index)); - } +void dump_config_helper(BaseTemplateSelect *this_, bool optimistic, bool has_lambda, const size_t initial_option_index, + bool restore_value) { + LOG_SELECT("", "Template Select", this_); + if (has_lambda) { + LOG_UPDATE_INTERVAL((PollingComponent *) this_); } else { - ESP_LOGD(TAG, "State from initial: %s", this->option_at(index)); - } - - this->publish_state(index); -} - -void TemplateSelect::update() { - if (!this->f_.has_value()) - return; - - auto val = this->f_(); - if (val.has_value()) { - if (!this->has_option(*val)) { - ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str()); - return; - } - this->publish_state(*val); + ESP_LOGCONFIG(TAG, + " Optimistic: %s\n" + " Initial Option: %s\n" + " Restore Value: %s", + YESNO(optimistic), this_->option_at(initial_option_index), YESNO(restore_value)); } } - -void TemplateSelect::control(size_t index) { - if (this->optimistic_) - this->publish_state(index); - - if (this->restore_value_) - this->pref_.save(&index); -} - -void TemplateSelectWithSetAction::control(size_t index) { - this->set_trigger_.trigger(StringRef(this->option_at(index))); - TemplateSelect::control(index); -} - -void TemplateSelect::dump_config() { - LOG_SELECT("", "Template Select", this); - LOG_UPDATE_INTERVAL(this); - if (this->f_.has_value()) - return; - ESP_LOGCONFIG(TAG, - " Optimistic: %s\n" - " Initial Option: %s\n" - " Restore Value: %s", - YESNO(this->optimistic_), this->option_at(this->initial_option_index_), YESNO(this->restore_value_)); -} - } // namespace esphome::template_ diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 2173d5ee24..8f1cacc5a0 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -8,38 +8,82 @@ #include "esphome/core/template_lambda.h" namespace esphome::template_ { +static const char *const TAG = "template.select"; +struct Empty {}; +class BaseTemplateSelect : public select::Select, public PollingComponent {}; + +void dump_config_helper(BaseTemplateSelect *this_, bool optimistic, bool has_lambda, size_t initial_option_index, + bool restore_value); /// Base template select class - used when no set_action is configured -class TemplateSelect : public select::Select, public PollingComponent { - public: - template void set_template(F &&f) { this->f_.set(std::forward(f)); } - void setup() override; - void update() override; - void dump_config() override; +template +class TemplateSelect : public BaseTemplateSelect { + public: + template void set_lambda(F &&f) { + if constexpr (HAS_LAMBDA) { + this->f_.set(std::forward(f)); + } + } + + void setup() override { + if constexpr (!HAS_LAMBDA) { + size_t index = INITIAL_OPTION_INDEX; + if constexpr (RESTORE_VALUE) { + this->pref_ = this->template make_entity_preference(); + if (this->pref_.load(&index) && this->has_index(index)) { + esph_log_d(TAG, "State from restore: %s", this->option_at(index)); + } else { + index = INITIAL_OPTION_INDEX; + esph_log_d(TAG, "State from initial (no valid stored index): %s", this->option_at(INITIAL_OPTION_INDEX)); + } + } else { + esph_log_d(TAG, "State from initial: %s", this->option_at(INITIAL_OPTION_INDEX)); + } + this->publish_state(index); + } + } + + void update() override { + if constexpr (HAS_LAMBDA) { + auto val = this->f_(); + if (val.has_value()) { + if (!this->has_option(*val)) { + esph_log_e(TAG, "Lambda returned an invalid option: %s", (*val).c_str()); + return; + } + this->publish_state(*val); + } + } + } + void dump_config() override { + dump_config_helper(this, OPTIMISTIC, HAS_LAMBDA, INITIAL_OPTION_INDEX, RESTORE_VALUE); + }; float get_setup_priority() const override { return setup_priority::HARDWARE; } - void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } - void set_initial_option_index(size_t initial_option_index) { this->initial_option_index_ = initial_option_index; } - void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } - protected: - void control(size_t index) override; - bool optimistic_ = false; - size_t initial_option_index_{0}; - bool restore_value_ = false; - TemplateLambda f_; - - ESPPreferenceObject pref_; + void control(size_t index) override { + if constexpr (OPTIMISTIC) + this->publish_state(index); + if constexpr (RESTORE_VALUE) + this->pref_.save(&index); + } + [[no_unique_address]] std::conditional_t, Empty> f_{}; + [[no_unique_address]] std::conditional_t pref_{}; }; /// Template select with set_action trigger - only instantiated when set_action is configured -class TemplateSelectWithSetAction final : public TemplateSelect { +template +class TemplateSelectWithSetAction final + : public TemplateSelect { public: Trigger *get_set_trigger() { return &this->set_trigger_; } protected: - void control(size_t index) override; + void control(size_t index) override { + this->set_trigger_.trigger(StringRef(this->option_at(index))); + TemplateSelect::control(index); + } Trigger set_trigger_; }; From cb9fbf8970ec48b4a2dfc486df3c12810238072f Mon Sep 17 00:00:00 2001 From: clydebarrow <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:56:21 +1100 Subject: [PATCH 2/3] Fix parameter name; set update_interval to never if no lambda to poll --- esphome/components/template/select/__init__.py | 11 ++++++++++- .../components/template/select/template_select.cpp | 10 +++++----- esphome/components/template/select/template_select.h | 2 +- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py index 566fb2996a..8de0b65c2d 100644 --- a/esphome/components/template/select/__init__.py +++ b/esphome/components/template/select/__init__.py @@ -10,7 +10,10 @@ from esphome.const import ( CONF_OPTIONS, CONF_RESTORE_VALUE, CONF_SET_ACTION, + CONF_UPDATE_INTERVAL, + SCHEDULER_DONT_RUN, ) +from esphome.core import TimePeriodMilliseconds from esphome.cpp_generator import TemplateArguments from .. import template_ns @@ -103,7 +106,13 @@ async def to_code(config): var_id, TemplateArguments(has_lambda, optimistic, restore_value, initial_option_index), ) - await cg.register_component(var, config) + component_config = config.copy() + if not has_lambda: + # No point in polling if not using a lambda + component_config[CONF_UPDATE_INTERVAL] = TimePeriodMilliseconds( + milliseconds=SCHEDULER_DONT_RUN + ) + await cg.register_component(var, component_config) await select.register_select(var, config, options=options) if CONF_LAMBDA in config: diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 5a7b6400c6..f6871cf186 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -3,17 +3,17 @@ namespace esphome::template_ { -void dump_config_helper(BaseTemplateSelect *this_, bool optimistic, bool has_lambda, const size_t initial_option_index, - bool restore_value) { - LOG_SELECT("", "Template Select", this_); +void dump_config_helper(BaseTemplateSelect *sel_comp, bool optimistic, bool has_lambda, + const size_t initial_option_index, bool restore_value) { + LOG_SELECT("", "Template Select", sel_comp); if (has_lambda) { - LOG_UPDATE_INTERVAL((PollingComponent *) this_); + LOG_UPDATE_INTERVAL(sel_comp); } else { ESP_LOGCONFIG(TAG, " Optimistic: %s\n" " Initial Option: %s\n" " Restore Value: %s", - YESNO(optimistic), this_->option_at(initial_option_index), YESNO(restore_value)); + YESNO(optimistic), sel_comp->option_at(initial_option_index), YESNO(restore_value)); } } } // namespace esphome::template_ diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 8f1cacc5a0..739d13e298 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -12,7 +12,7 @@ static const char *const TAG = "template.select"; struct Empty {}; class BaseTemplateSelect : public select::Select, public PollingComponent {}; -void dump_config_helper(BaseTemplateSelect *this_, bool optimistic, bool has_lambda, size_t initial_option_index, +void dump_config_helper(BaseTemplateSelect *sel_comp, bool optimistic, bool has_lambda, size_t initial_option_index, bool restore_value); /// Base template select class - used when no set_action is configured From 2394ac276cfaec0943dc397495dfff0363ae1cfc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Feb 2026 11:39:14 +0100 Subject: [PATCH 3/3] avoid duplicating --- .../template/select/template_select.cpp | 29 +++++++++++++++++++ .../template/select/template_select.h | 25 +++++----------- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index f6871cf186..e68729c2d4 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -3,6 +3,8 @@ namespace esphome::template_ { +static const char *const TAG = "template.select"; + void dump_config_helper(BaseTemplateSelect *sel_comp, bool optimistic, bool has_lambda, const size_t initial_option_index, bool restore_value) { LOG_SELECT("", "Template Select", sel_comp); @@ -16,4 +18,31 @@ void dump_config_helper(BaseTemplateSelect *sel_comp, bool optimistic, bool has_ YESNO(optimistic), sel_comp->option_at(initial_option_index), YESNO(restore_value)); } } + +void setup_initial(BaseTemplateSelect *sel_comp, size_t initial_index) { + ESP_LOGD(TAG, "State from initial: %s", sel_comp->option_at(initial_index)); + sel_comp->publish_state(initial_index); +} + +void setup_with_restore(BaseTemplateSelect *sel_comp, ESPPreferenceObject &pref, size_t initial_index) { + size_t index = initial_index; + if (pref.load(&index) && sel_comp->has_index(index)) { + ESP_LOGD(TAG, "State from restore: %s", sel_comp->option_at(index)); + } else { + index = initial_index; + ESP_LOGD(TAG, "State from initial (no valid stored index): %s", sel_comp->option_at(initial_index)); + } + sel_comp->publish_state(index); +} + +void update_lambda(BaseTemplateSelect *sel_comp, const optional &val) { + if (val.has_value()) { + if (!sel_comp->has_option(*val)) { + ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str()); + return; + } + sel_comp->publish_state(*val); + } +} + } // namespace esphome::template_ diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 739d13e298..5da6d732bd 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -8,12 +8,15 @@ #include "esphome/core/template_lambda.h" namespace esphome::template_ { -static const char *const TAG = "template.select"; + struct Empty {}; class BaseTemplateSelect : public select::Select, public PollingComponent {}; void dump_config_helper(BaseTemplateSelect *sel_comp, bool optimistic, bool has_lambda, size_t initial_option_index, bool restore_value); +void setup_initial(BaseTemplateSelect *sel_comp, size_t initial_index); +void setup_with_restore(BaseTemplateSelect *sel_comp, ESPPreferenceObject &pref, size_t initial_index); +void update_lambda(BaseTemplateSelect *sel_comp, const optional &val); /// Base template select class - used when no set_action is configured @@ -28,32 +31,18 @@ class TemplateSelect : public BaseTemplateSelect { void setup() override { if constexpr (!HAS_LAMBDA) { - size_t index = INITIAL_OPTION_INDEX; if constexpr (RESTORE_VALUE) { this->pref_ = this->template make_entity_preference(); - if (this->pref_.load(&index) && this->has_index(index)) { - esph_log_d(TAG, "State from restore: %s", this->option_at(index)); - } else { - index = INITIAL_OPTION_INDEX; - esph_log_d(TAG, "State from initial (no valid stored index): %s", this->option_at(INITIAL_OPTION_INDEX)); - } + setup_with_restore(this, this->pref_, INITIAL_OPTION_INDEX); } else { - esph_log_d(TAG, "State from initial: %s", this->option_at(INITIAL_OPTION_INDEX)); + setup_initial(this, INITIAL_OPTION_INDEX); } - this->publish_state(index); } } void update() override { if constexpr (HAS_LAMBDA) { - auto val = this->f_(); - if (val.has_value()) { - if (!this->has_option(*val)) { - esph_log_e(TAG, "Lambda returned an invalid option: %s", (*val).c_str()); - return; - } - this->publish_state(*val); - } + update_lambda(this, this->f_()); } } void dump_config() override {