[climate] Fold ControlAction fields into a single stateless lambda (#16044)

This commit is contained in:
J. Nick Koston
2026-04-30 19:16:16 -05:00
committed by GitHub
parent 24fdfcf1a1
commit 3d69169141
4 changed files with 238 additions and 60 deletions

View File

@@ -0,0 +1,92 @@
esphome:
name: climate-control-action-test
host:
api:
logger:
level: DEBUG
globals:
- id: test_target_temp
type: float
initial_value: "21.5"
sensor:
- platform: template
id: temp_sensor
name: "Temp"
lambda: 'return 20.0;'
update_interval: 60s
climate:
- platform: thermostat
id: test_climate
name: "Test Climate"
sensor: temp_sensor
min_idle_time: 30s
min_heating_off_time: 300s
min_heating_run_time: 300s
min_cooling_off_time: 300s
min_cooling_run_time: 300s
heat_action:
- logger.log: heating
idle_action:
- logger.log: idle
cool_action:
- logger.log: cooling
heat_cool_mode:
- logger.log: heat_cool
preset:
- name: Default
default_target_temperature_low: 18 °C
default_target_temperature_high: 22 °C
visual:
min_temperature: 10 °C
max_temperature: 30 °C
button:
# mode only
- platform: template
id: btn_mode
name: "Set Mode Heat"
on_press:
- climate.control:
id: test_climate
mode: HEAT
# mode + target_temperature_low + target_temperature_high
- platform: template
id: btn_mode_temps
name: "Set Mode Temps"
on_press:
- climate.control:
id: test_climate
mode: HEAT_COOL
target_temperature_low: 19.0 °C
target_temperature_high: 23.0 °C
# target_temperature_low only
- platform: template
id: btn_low_only
name: "Set Low Only"
on_press:
- climate.control:
id: test_climate
target_temperature_low: 17.5 °C
# Lambda path: target_temperature_high computed at runtime
- platform: template
id: btn_lambda_high
name: "Lambda High"
on_press:
- climate.control:
id: test_climate
target_temperature_high: !lambda "return id(test_target_temp);"
# mode only — turn off via mode
- platform: template
id: btn_off
name: "Set Off"
on_press:
- climate.control:
id: test_climate
mode: "OFF"

View File

@@ -0,0 +1,84 @@
"""Integration test for climate ControlAction.
Tests that climate.control automation actions work correctly with the
single stateless apply lambda/function pointer implementation. Exercises
multiple field combinations and the lambda path.
"""
from __future__ import annotations
import asyncio
from aioesphomeapi import (
ButtonInfo,
ClimateInfo,
ClimateMode,
ClimateState,
EntityState,
)
import pytest
from .state_utils import InitialStateHelper, require_entity
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_climate_control_action(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test climate ControlAction with constants and lambdas."""
loop = asyncio.get_running_loop()
async with run_compiled(yaml_config), api_client_connected() as client:
climate_state_future: asyncio.Future[ClimateState] | None = None
def on_state(state: EntityState) -> None:
if (
isinstance(state, ClimateState)
and climate_state_future is not None
and not climate_state_future.done()
):
climate_state_future.set_result(state)
async def wait_for_climate_state(timeout: float = 5.0) -> ClimateState:
nonlocal climate_state_future
climate_state_future = loop.create_future()
try:
return await asyncio.wait_for(climate_state_future, timeout)
finally:
climate_state_future = None
entities, _ = await client.list_entities_services()
initial_state_helper = InitialStateHelper(entities)
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
await initial_state_helper.wait_for_initial_states()
require_entity(entities, "test_climate", ClimateInfo)
async def press_and_wait(name: str) -> ClimateState:
btn = require_entity(entities, name.lower().replace(" ", "_"), ButtonInfo)
client.button_command(btn.key)
return await wait_for_climate_state()
# mode only — set HEAT
state = await press_and_wait("Set Mode Heat")
assert state.mode == ClimateMode.HEAT
# mode + target_temperature_low + target_temperature_high
state = await press_and_wait("Set Mode Temps")
assert state.mode == ClimateMode.HEAT_COOL
assert state.target_temperature_low == pytest.approx(19.0, abs=0.5)
assert state.target_temperature_high == pytest.approx(23.0, abs=0.5)
# target_temperature_low only
state = await press_and_wait("Set Low Only")
assert state.target_temperature_low == pytest.approx(17.5, abs=0.5)
# lambda path: target_temperature_high computed at runtime
state = await press_and_wait("Lambda High")
assert state.target_temperature_high == pytest.approx(21.5, abs=0.5)
# mode only — turn off via mode
state = await press_and_wait("Set Off")
assert state.mode == ClimateMode.OFF