From eb7aa3420fcf0d649953a0ce6c8643c0a2357e31 Mon Sep 17 00:00:00 2001 From: tronikos Date: Fri, 6 Feb 2026 12:23:42 -0800 Subject: [PATCH] Add target_temperature to the template water heater (#13661) Co-authored-by: J. Nick Koston --- .../components/template/water_heater/__init__.py | 9 +++++++++ .../water_heater/template_water_heater.cpp | 14 +++++++++++++- .../template/water_heater/template_water_heater.h | 4 ++++ tests/components/template/common-base.yaml | 1 + .../fixtures/water_heater_template.yaml | 1 + tests/integration/test_water_heater_template.py | 3 +++ 6 files changed, 31 insertions(+), 1 deletion(-) diff --git a/esphome/components/template/water_heater/__init__.py b/esphome/components/template/water_heater/__init__.py index bddd378b23..5f96155fbf 100644 --- a/esphome/components/template/water_heater/__init__.py +++ b/esphome/components/template/water_heater/__init__.py @@ -46,6 +46,7 @@ CONFIG_SCHEMA = ( RESTORE_MODES, upper=True ), cv.Optional(CONF_CURRENT_TEMPERATURE): cv.returning_lambda, + cv.Optional(CONF_TARGET_TEMPERATURE): cv.returning_lambda, cv.Optional(CONF_MODE): cv.returning_lambda, cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list( water_heater.validate_water_heater_mode @@ -78,6 +79,14 @@ async def to_code(config: ConfigType) -> None: ) cg.add(var.set_current_temperature_lambda(template_)) + if CONF_TARGET_TEMPERATURE in config: + template_ = await cg.process_lambda( + config[CONF_TARGET_TEMPERATURE], + [], + return_type=cg.optional.template(cg.float_), + ) + cg.add(var.set_target_temperature_lambda(template_)) + if CONF_MODE in config: template_ = await cg.process_lambda( config[CONF_MODE], diff --git a/esphome/components/template/water_heater/template_water_heater.cpp b/esphome/components/template/water_heater/template_water_heater.cpp index f888edb1df..c354deee0e 100644 --- a/esphome/components/template/water_heater/template_water_heater.cpp +++ b/esphome/components/template/water_heater/template_water_heater.cpp @@ -16,7 +16,8 @@ void TemplateWaterHeater::setup() { restore->perform(); } } - if (!this->current_temperature_f_.has_value() && !this->mode_f_.has_value()) + if (!this->current_temperature_f_.has_value() && !this->target_temperature_f_.has_value() && + !this->mode_f_.has_value()) this->disable_loop(); } @@ -28,6 +29,9 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() { } traits.set_supports_current_temperature(true); + if (this->target_temperature_f_.has_value()) { + traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_TARGET_TEMPERATURE); + } return traits; } @@ -42,6 +46,14 @@ void TemplateWaterHeater::loop() { } } + auto target_temp = this->target_temperature_f_.call(); + if (target_temp.has_value()) { + if (*target_temp != this->target_temperature_) { + this->target_temperature_ = *target_temp; + changed = true; + } + } + auto new_mode = this->mode_f_.call(); if (new_mode.has_value()) { if (*new_mode != this->mode_) { diff --git a/esphome/components/template/water_heater/template_water_heater.h b/esphome/components/template/water_heater/template_water_heater.h index f1cf00a115..22173209aa 100644 --- a/esphome/components/template/water_heater/template_water_heater.h +++ b/esphome/components/template/water_heater/template_water_heater.h @@ -20,6 +20,9 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater { template void set_current_temperature_lambda(F &&f) { this->current_temperature_f_.set(std::forward(f)); } + template void set_target_temperature_lambda(F &&f) { + this->target_temperature_f_.set(std::forward(f)); + } template void set_mode_lambda(F &&f) { this->mode_f_.set(std::forward(f)); } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } @@ -44,6 +47,7 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater { // Ordered to minimize padding on 32-bit: 4-byte members first, then smaller Trigger<> set_trigger_; TemplateLambda current_temperature_f_; + TemplateLambda target_temperature_f_; TemplateLambda mode_f_; TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE}; water_heater::WaterHeaterModeMask supported_modes_; diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index d1849efaf7..b8742f8c7b 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -412,6 +412,7 @@ water_heater: name: "Template Water Heater" optimistic: true current_temperature: !lambda "return 42.0f;" + target_temperature: !lambda "return 60.0f;" mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;" supported_modes: - "OFF" diff --git a/tests/integration/fixtures/water_heater_template.yaml b/tests/integration/fixtures/water_heater_template.yaml index b54ebed789..1aaded1991 100644 --- a/tests/integration/fixtures/water_heater_template.yaml +++ b/tests/integration/fixtures/water_heater_template.yaml @@ -10,6 +10,7 @@ water_heater: name: Test Boiler optimistic: true current_temperature: !lambda "return 45.0f;" + target_temperature: !lambda "return 60.0f;" # Note: No mode lambda - we want optimistic mode changes to stick # A mode lambda would override mode changes in loop() supported_modes: diff --git a/tests/integration/test_water_heater_template.py b/tests/integration/test_water_heater_template.py index b5f1fb64c0..6b4a685d0d 100644 --- a/tests/integration/test_water_heater_template.py +++ b/tests/integration/test_water_heater_template.py @@ -85,6 +85,9 @@ async def test_water_heater_template( assert initial_state.current_temperature == 45.0, ( f"Expected current temp 45.0, got {initial_state.current_temperature}" ) + assert initial_state.target_temperature == 60.0, ( + f"Expected target temp 60.0, got {initial_state.target_temperature}" + ) # Test changing to GAS mode client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.GAS)