From 08c1659d5b107fdfe007d0587e6faa993f1165f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Oct 2026 20:06:05 -1000 Subject: [PATCH] Convert climate.control and water_heater.template.publish temperatures into the entity unit --- esphome/components/climate/__init__.py | 37 +++++++++++++++---- .../components/template/climate/__init__.py | 16 ++------ .../template/water_heater/__init__.py | 26 ++++++++----- esphome/components/water_heater/__init__.py | 10 ++++- tests/components/template/common-base.yaml | 7 ++++ 5 files changed, 67 insertions(+), 29 deletions(-) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 641a9f67601..10c901d63dc 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -170,7 +170,15 @@ def temperature_unit_of_id(climate_id: ID) -> str: for conf in CORE.config.get(DOMAIN, []): if conf[CONF_ID] == climate_id: return temperature_unit_of(conf) - return UNIT_CELSIUS + raise KeyError(f"No climate with id {climate_id}") + + +def temperature_in_entity_unit( + config: ConfigType, value: tuple[float, str | None] +) -> str: + """Render a static action temperature in the unit of the climate the action targets.""" + unit = temperature_unit_of_id(config[CONF_ID]) + return str(cg.safe_exp(cv.convert_temperature(*value, unit))) def _visual_temperatures_in_entity_unit(config: ConfigType) -> ConfigType: @@ -493,9 +501,13 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema( { cv.Required(CONF_ID): cv.use_id(Climate), cv.Optional(CONF_MODE): cv.templatable(validate_climate_mode), - cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature), - cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature), - cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature), + cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature_with_unit), + cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable( + cv.temperature_with_unit + ), + cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable( + cv.temperature_with_unit + ), cv.Optional(CONF_TARGET_HUMIDITY): cv.templatable(cv.percentage_int), cv.Optional(CONF_AWAY): cv.invalid("Use preset instead"), cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable( @@ -515,12 +527,23 @@ automation.register_apply_action( "climate.control", CLIMATE_CONTROL_ACTION_SCHEMA, automation.ApplyField(CONF_MODE, "set_mode", ClimateMode), - automation.ApplyField(CONF_TARGET_TEMPERATURE, "set_target_temperature", cg.float_), automation.ApplyField( - CONF_TARGET_TEMPERATURE_LOW, "set_target_temperature_low", cg.float_ + CONF_TARGET_TEMPERATURE, + "set_target_temperature", + cg.float_, + temperature_in_entity_unit, ), automation.ApplyField( - CONF_TARGET_TEMPERATURE_HIGH, "set_target_temperature_high", cg.float_ + CONF_TARGET_TEMPERATURE_LOW, + "set_target_temperature_low", + cg.float_, + temperature_in_entity_unit, + ), + automation.ApplyField( + CONF_TARGET_TEMPERATURE_HIGH, + "set_target_temperature_high", + cg.float_, + temperature_in_entity_unit, ), automation.ApplyField(CONF_TARGET_HUMIDITY, "set_target_humidity", cg.float_), automation.ApplyField(CONF_FAN_MODE, "set_fan_mode", ClimateFanMode), diff --git a/esphome/components/template/climate/__init__.py b/esphome/components/template/climate/__init__.py index 3c853d4cbff..873ade96128 100644 --- a/esphome/components/template/climate/__init__.py +++ b/esphome/components/template/climate/__init__.py @@ -433,14 +433,6 @@ CLIMATE_TEMPLATE_PUBLISH_ACTION_SCHEMA = cv.All( ) -def _temperature_in_entity_unit( - config: ConfigType, value: tuple[float, str | None] -) -> str: - # The entity's unit is only known once every climate has been validated - unit = climate.temperature_unit_of_id(config[CONF_ID]) - return str(cg.safe_exp(cv.convert_temperature(*value, unit))) - - automation.register_apply_action( "climate.template.publish", CLIMATE_TEMPLATE_PUBLISH_ACTION_SCHEMA, @@ -448,26 +440,26 @@ automation.register_apply_action( CONF_CURRENT_TEMPERATURE, "current_temperature = {}", cg.float_, - _temperature_in_entity_unit, + climate.temperature_in_entity_unit, ), automation.ApplyField(CONF_CURRENT_HUMIDITY, "current_humidity = {}", cg.float_), automation.ApplyField( CONF_TARGET_TEMPERATURE, "set_target_temperature", cg.float_, - _temperature_in_entity_unit, + climate.temperature_in_entity_unit, ), automation.ApplyField( CONF_TARGET_TEMPERATURE_LOW, "set_target_temperature_low", cg.float_, - _temperature_in_entity_unit, + climate.temperature_in_entity_unit, ), automation.ApplyField( CONF_TARGET_TEMPERATURE_HIGH, "set_target_temperature_high", cg.float_, - _temperature_in_entity_unit, + climate.temperature_in_entity_unit, ), automation.ApplyField(CONF_TARGET_HUMIDITY, "set_target_humidity", cg.float_), automation.ApplyField(CONF_MODE, "set_mode", climate.ClimateMode), diff --git a/esphome/components/template/water_heater/__init__.py b/esphome/components/template/water_heater/__init__.py index 5e1dfe008c3..923248e8c31 100644 --- a/esphome/components/template/water_heater/__init__.py +++ b/esphome/components/template/water_heater/__init__.py @@ -129,8 +129,12 @@ async def to_code(config: ConfigType) -> None: cv.Schema( { cv.GenerateID(): cv.use_id(TemplateWaterHeater), - cv.Optional(CONF_CURRENT_TEMPERATURE): cv.templatable(cv.temperature), - cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature), + cv.Optional(CONF_CURRENT_TEMPERATURE): cv.templatable( + cv.temperature_with_unit + ), + cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable( + cv.temperature_with_unit + ), cv.Optional(CONF_MODE): cv.templatable( water_heater.validate_water_heater_mode ), @@ -149,13 +153,17 @@ async def water_heater_template_publish_to_code( var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - if current_temp := config.get(CONF_CURRENT_TEMPERATURE): - template_ = await cg.templatable(current_temp, args, cg.float_) - cg.add(var.set_current_temperature(template_)) - - if target_temp := config.get(CONF_TARGET_TEMPERATURE): - template_ = await cg.templatable(target_temp, args, cg.float_) - cg.add(var.set_target_temperature(template_)) + # A static temperature is in the unit of the water heater it is published to + unit = water_heater.temperature_unit_of_id(config[CONF_ID]) + for key, setter in ( + (CONF_CURRENT_TEMPERATURE, var.set_current_temperature), + (CONF_TARGET_TEMPERATURE, var.set_target_temperature), + ): + if (value := config.get(key)) is None: + continue + if isinstance(value, tuple): + value = cv.convert_temperature(*value, unit) + cg.add(setter(await cg.templatable(value, args, cg.float_))) if mode := config.get(CONF_MODE): template_ = await cg.templatable(mode, args, water_heater.WaterHeaterMode) diff --git a/esphome/components/water_heater/__init__.py b/esphome/components/water_heater/__init__.py index a44a788d65a..a54bb35b6c4 100644 --- a/esphome/components/water_heater/__init__.py +++ b/esphome/components/water_heater/__init__.py @@ -10,7 +10,7 @@ from esphome.const import ( CONF_VISUAL, UNIT_CELSIUS, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, queue_entity_register, @@ -52,6 +52,14 @@ def temperature_unit_of(config: ConfigType) -> str: return unit if unit in TEMPERATURE_UNITS else UNIT_CELSIUS +def temperature_unit_of_id(water_heater_id: ID) -> str: + """The unit of the water heater with this id, for actions that take static temperatures.""" + for conf in CORE.config.get(DOMAIN, []): + if conf[CONF_ID] == water_heater_id: + return temperature_unit_of(conf) + raise KeyError(f"No water heater with id {water_heater_id}") + + def _visual_temperatures_in_entity_unit(config: ConfigType) -> ConfigType: """Convert the visual temperatures into the entity's unit.""" unit = temperature_unit_of(config) diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index e850196949c..70c20be9a97 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -45,6 +45,13 @@ esphome: id: template_climate_fahrenheit current_temperature: 20°C target_temperature: 70 + - climate.control: + id: template_climate_fahrenheit + target_temperature: 22°C + - water_heater.template.publish: + id: template_water_heater_fahrenheit + current_temperature: 45°C + target_temperature: 120 - climate.template.publish: id: template_climate_custom_modes custom_fan_mode: "turbo"