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] 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 da57e94b07a..566fb2996a3 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 0849e1a434a..5a7b6400c6f 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 2173d5ee24e..8f1cacc5a0a 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_; };