From 9ca394d1e5491dbef47923a1447f010b6cde0fb1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 02:31:29 +0100 Subject: [PATCH 1/6] not as bad as I was thinking it would be --- .../components/template/select/__init__.py | 8 ++++++-- .../template/select/template_select.cpp | 9 +++++---- .../template/select/template_select.h | 19 ++++++++++++------- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py index 08c489a3f3..fd2fd62f59 100644 --- a/esphome/components/template/select/__init__.py +++ b/esphome/components/template/select/__init__.py @@ -17,6 +17,9 @@ from .. import template_ns TemplateSelect = template_ns.class_( "TemplateSelect", select.Select, cg.PollingComponent ) +TemplateSelectWithSetAction = template_ns.class_( + "TemplateSelectWithSetAction", TemplateSelect +) def validate(config): @@ -62,7 +65,9 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + # Use subclass with trigger only when set_action is configured + cls = TemplateSelectWithSetAction if CONF_SET_ACTION in config else TemplateSelect + var = cg.new_Pvariable(config[CONF_ID], cls) await cg.register_component(var, config) await select.register_select(var, config, options=config[CONF_OPTIONS]) @@ -87,7 +92,6 @@ async def to_code(config): cg.add(var.set_restore_value(True)) if CONF_SET_ACTION in config: - cg.add_define("USE_TEMPLATE_SELECT_SET_TRIGGER") await automation.build_automation( var.get_set_trigger(), [(cg.StringRef, "x")], config[CONF_SET_ACTION] ) diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 2d1f48157f..0849e1a434 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -41,10 +41,6 @@ void TemplateSelect::update() { } void TemplateSelect::control(size_t index) { -#ifdef USE_TEMPLATE_SELECT_SET_TRIGGER - this->set_trigger_->trigger(StringRef(this->option_at(index))); -#endif - if (this->optimistic_) this->publish_state(index); @@ -52,6 +48,11 @@ void TemplateSelect::control(size_t index) { 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); diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index ff5aff7bd3..2173d5ee24 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -9,7 +9,8 @@ namespace esphome::template_ { -class TemplateSelect final : public select::Select, public PollingComponent { +/// 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)); } @@ -18,9 +19,6 @@ class TemplateSelect final : public select::Select, public PollingComponent { void dump_config() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } -#ifdef USE_TEMPLATE_SELECT_SET_TRIGGER - Trigger *get_set_trigger() const { return this->set_trigger_; } -#endif 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; } @@ -30,12 +28,19 @@ class TemplateSelect final : public select::Select, public PollingComponent { bool optimistic_ = false; size_t initial_option_index_{0}; bool restore_value_ = false; -#ifdef USE_TEMPLATE_SELECT_SET_TRIGGER - Trigger *set_trigger_ = new Trigger(); -#endif TemplateLambda f_; ESPPreferenceObject pref_; }; +/// Template select with set_action trigger - only instantiated when set_action is configured +class TemplateSelectWithSetAction final : public TemplateSelect { + public: + Trigger *get_set_trigger() { return &this->set_trigger_; } + + protected: + void control(size_t index) override; + Trigger set_trigger_; +}; + } // namespace esphome::template_ From cfc3b3336f77627e02d10df6a952c28c50ce7fef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 02:35:51 +0100 Subject: [PATCH 2/6] fix --- esphome/components/template/select/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py index fd2fd62f59..101b4ade60 100644 --- a/esphome/components/template/select/__init__.py +++ b/esphome/components/template/select/__init__.py @@ -66,8 +66,9 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): # Use subclass with trigger only when set_action is configured - cls = TemplateSelectWithSetAction if CONF_SET_ACTION in config else TemplateSelect - var = cg.new_Pvariable(config[CONF_ID], cls) + if CONF_SET_ACTION in config: + config[CONF_ID].type = TemplateSelectWithSetAction + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await select.register_select(var, config, options=config[CONF_OPTIONS]) From 48e6efb6aa0b4f5f742b1c512a447dc9fa882eac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 02:40:30 +0100 Subject: [PATCH 3/6] use pattern from sensor filters --- esphome/components/template/select/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py index 101b4ade60..8de7bce83d 100644 --- a/esphome/components/template/select/__init__.py +++ b/esphome/components/template/select/__init__.py @@ -66,9 +66,11 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): # Use subclass with trigger only when set_action is configured + select_id = config[CONF_ID] if CONF_SET_ACTION in config: - config[CONF_ID].type = TemplateSelectWithSetAction - var = cg.new_Pvariable(config[CONF_ID]) + select_id = select_id.copy() + select_id.type = TemplateSelectWithSetAction + var = cg.new_Pvariable(select_id) await cg.register_component(var, config) await select.register_select(var, config, options=config[CONF_OPTIONS]) From 6c853cae57e481893c24e6ced566f0597847b553 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 02:40:45 +0100 Subject: [PATCH 4/6] use pattern from sensor filters --- esphome/components/template/select/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py index 8de7bce83d..08c7c07132 100644 --- a/esphome/components/template/select/__init__.py +++ b/esphome/components/template/select/__init__.py @@ -66,11 +66,10 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): # Use subclass with trigger only when set_action is configured - select_id = config[CONF_ID] if CONF_SET_ACTION in config: - select_id = select_id.copy() - select_id.type = TemplateSelectWithSetAction - var = cg.new_Pvariable(select_id) + config[CONF_ID] = config[CONF_ID].copy() + config[CONF_ID].type = TemplateSelectWithSetAction + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await select.register_select(var, config, options=config[CONF_OPTIONS]) From 9dbcf1447b57db32f5986718a1575460445d77ba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 02:45:43 +0100 Subject: [PATCH 5/6] integration test --- .../fixtures/select_stringref_trigger.yaml | 16 ++++++++++++- .../test_select_stringref_trigger.py | 24 ++++++++++++------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/tests/integration/fixtures/select_stringref_trigger.yaml b/tests/integration/fixtures/select_stringref_trigger.yaml index bb1e1fd843..5858e2f529 100644 --- a/tests/integration/fixtures/select_stringref_trigger.yaml +++ b/tests/integration/fixtures/select_stringref_trigger.yaml @@ -56,7 +56,21 @@ select: std::string prefix = x.substr(0, 6); ESP_LOGI("test", "Substr prefix: %s", prefix.c_str()); - # Second select with numeric options to test ADL functions + # Second select with set_action trigger (uses TemplateSelectWithSetAction subclass) + - platform: template + name: "Action Select" + id: action_select + options: + - "Action A" + - "Action B" + set_action: + then: + # Test: set_action trigger receives StringRef + - logger.log: + format: "set_action triggered: %s" + args: ['x.c_str()'] + + # Third select with numeric options to test ADL functions - platform: template name: "Baud Rate" id: baud_select diff --git a/tests/integration/test_select_stringref_trigger.py b/tests/integration/test_select_stringref_trigger.py index 7fc72a2290..5baba9c7f5 100644 --- a/tests/integration/test_select_stringref_trigger.py +++ b/tests/integration/test_select_stringref_trigger.py @@ -28,6 +28,8 @@ async def test_select_stringref_trigger( find_substr_future = loop.create_future() find_char_future = loop.create_future() substr_future = loop.create_future() + # set_action trigger (TemplateSelectWithSetAction subclass) + set_action_future = loop.create_future() # ADL functions stoi_future = loop.create_future() stol_future = loop.create_future() @@ -43,6 +45,8 @@ async def test_select_stringref_trigger( find_substr_pattern = re.compile(r"Found 'Option' in value") find_char_pattern = re.compile(r"Space at position: 6") # space at index 6 substr_pattern = re.compile(r"Substr prefix: Option") + # set_action trigger pattern (TemplateSelectWithSetAction subclass) + set_action_pattern = re.compile(r"set_action triggered: Action B") # ADL function patterns (115200 from baud rate select) stoi_pattern = re.compile(r"stoi result: 115200") stol_pattern = re.compile(r"stol result: 115200") @@ -67,6 +71,9 @@ async def test_select_stringref_trigger( find_char_future.set_result(True) if not substr_future.done() and substr_pattern.search(line): substr_future.set_result(True) + # set_action trigger + if not set_action_future.done() and set_action_pattern.search(line): + set_action_future.set_result(True) # ADL functions if not stoi_future.done() and stoi_pattern.search(line): stoi_future.set_result(True) @@ -89,22 +96,21 @@ async def test_select_stringref_trigger( # List entities to find our select entities, _ = await client.list_entities_services() - select_entity = next( - (e for e in entities if hasattr(e, "options") and e.name == "Test Select"), - None, - ) + select_entity = next((e for e in entities if e.name == "Test Select"), None) assert select_entity is not None, "Test Select entity not found" - baud_entity = next( - (e for e in entities if hasattr(e, "options") and e.name == "Baud Rate"), - None, - ) + baud_entity = next((e for e in entities if e.name == "Baud Rate"), None) assert baud_entity is not None, "Baud Rate entity not found" + action_entity = next((e for e in entities if e.name == "Action Select"), None) + assert action_entity is not None, "Action Select entity not found" + # Change select to Option B - this should trigger on_value with StringRef client.select_command(select_entity.key, "Option B") # Change baud to 115200 - this tests ADL functions (stoi, stol, stof, stod) client.select_command(baud_entity.key, "115200") + # Change action select - tests set_action trigger (TemplateSelectWithSetAction) + client.select_command(action_entity.key, "Action B") # Wait for all log messages confirming StringRef operations work try: @@ -118,6 +124,7 @@ async def test_select_stringref_trigger( find_substr_future, find_char_future, substr_future, + set_action_future, stoi_future, stol_future, stof_future, @@ -135,6 +142,7 @@ async def test_select_stringref_trigger( "find_substr": find_substr_future.done(), "find_char": find_char_future.done(), "substr": substr_future.done(), + "set_action": set_action_future.done(), "stoi": stoi_future.done(), "stol": stol_future.done(), "stof": stof_future.done(), From 89bd9b610e576b357e2697ab9f561cd75b833317 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Feb 2026 02:48:00 +0100 Subject: [PATCH 6/6] modify in validation instead to avoid copy --- esphome/components/template/select/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py index 08c7c07132..da57e94b07 100644 --- a/esphome/components/template/select/__init__.py +++ b/esphome/components/template/select/__init__.py @@ -42,6 +42,11 @@ def validate(config): raise 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 + return config @@ -65,10 +70,6 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - # Use subclass with trigger only when set_action is configured - if CONF_SET_ACTION in config: - config[CONF_ID] = config[CONF_ID].copy() - config[CONF_ID].type = TemplateSelectWithSetAction var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await select.register_select(var, config, options=config[CONF_OPTIONS])