mirror of
https://github.com/esphome/esphome.git
synced 2026-09-12 15:57:33 +00:00
119 lines
5.1 KiB
Python
119 lines
5.1 KiB
Python
"""Integration tests for template climate: two-point target temperature + humidity.
|
|
|
|
Covers the supports_two_point_target_temperature/supports_target_humidity boolean flags plus
|
|
on_control (forwarding commands out) and climate.template.publish (the device reporting its own
|
|
authoritative state, independent of any prior command -- e.g. a device that owns its own setpoint,
|
|
changed via a physical remote).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
|
|
import aioesphomeapi
|
|
from aioesphomeapi import ButtonInfo, ClimateInfo, ClimateMode
|
|
import pytest
|
|
|
|
from .host_prefs import clear_host_prefs
|
|
from .state_utils import InitialStateHelper, require_entity, wait_for_state
|
|
from .types import APIClientConnectedFactory, RunCompiledFunction
|
|
|
|
DEVICE_NAME = "tmpl-clim-two-point"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_template_climate_two_point_temperature(
|
|
yaml_config: str,
|
|
run_compiled: RunCompiledFunction,
|
|
api_client_connected: APIClientConnectedFactory,
|
|
) -> None:
|
|
"""Two-point target temperature + humidity: booleans, on_control, and publish precedence."""
|
|
clear_host_prefs(DEVICE_NAME)
|
|
|
|
log_lines: list[str] = []
|
|
|
|
def on_log_line(line: str) -> None:
|
|
if "on_control " in line:
|
|
log_lines.append(line)
|
|
|
|
async with (
|
|
run_compiled(yaml_config, line_callback=on_log_line),
|
|
api_client_connected() as client,
|
|
):
|
|
|
|
async def wait_for_climate_state(
|
|
timeout: float = 5.0,
|
|
) -> aioesphomeapi.ClimateState:
|
|
return await wait_for_state(
|
|
client, lambda s: isinstance(s, aioesphomeapi.ClimateState), timeout
|
|
)
|
|
|
|
entities, _ = await client.list_entities_services()
|
|
initial_state_helper = InitialStateHelper(entities)
|
|
climate_infos = [e for e in entities if isinstance(e, ClimateInfo)]
|
|
assert len(climate_infos) == 1, "Expected exactly 1 climate entity"
|
|
|
|
test_climate = climate_infos[0]
|
|
assert test_climate.name == "Test Two-Point Heatpump"
|
|
assert test_climate.supports_two_point_target_temperature
|
|
assert test_climate.supports_target_humidity
|
|
|
|
report_button = require_entity(entities, "simulate_device_report", ButtonInfo)
|
|
|
|
client.subscribe_states(
|
|
initial_state_helper.on_state_wrapper(lambda state: None)
|
|
)
|
|
|
|
try:
|
|
await initial_state_helper.wait_for_initial_states()
|
|
except TimeoutError:
|
|
pytest.fail("Timeout waiting for initial states")
|
|
|
|
initial = initial_state_helper.initial_states.get(test_climate.key)
|
|
assert initial is not None, "No initial climate state received"
|
|
assert isinstance(initial, aioesphomeapi.ClimateState)
|
|
# Nothing has been published yet: settable fields have no sensor to seed them from, so
|
|
# the entity starts at ESPHome's plain defaults. current_temperature is pushed by the
|
|
# referenced sensor, which has already settled by the time we get here.
|
|
assert initial.mode == ClimateMode.OFF
|
|
assert initial.current_temperature == pytest.approx(21.0, abs=0.1)
|
|
|
|
# The device reports its actual state for the first time.
|
|
client.button_command(report_button.key)
|
|
state = await wait_for_climate_state()
|
|
assert state.mode == ClimateMode.HEAT_COOL
|
|
assert state.target_temperature_low == pytest.approx(18.0, abs=0.1)
|
|
assert state.target_temperature_high == pytest.approx(24.0, abs=0.1)
|
|
assert state.target_humidity == pytest.approx(50.0, abs=0.1)
|
|
|
|
# Commands apply optimistically (settable fields are plain internal state), and on_control
|
|
# fires with the same values so a real config could forward them to the device.
|
|
client.climate_command(
|
|
test_climate.key, target_temperature_low=19.0, target_temperature_high=25.0
|
|
)
|
|
state = await wait_for_climate_state()
|
|
assert state.target_temperature_low == pytest.approx(19.0, abs=0.1)
|
|
assert state.target_temperature_high == pytest.approx(25.0, abs=0.1)
|
|
await asyncio.sleep(0.2)
|
|
assert any(
|
|
"on_control target_temperature_low=19.0" in line for line in log_lines
|
|
)
|
|
assert any(
|
|
"on_control target_temperature_high=25.0" in line for line in log_lines
|
|
)
|
|
|
|
client.climate_command(test_climate.key, target_humidity=45.0)
|
|
state = await wait_for_climate_state()
|
|
assert state.target_humidity == pytest.approx(45.0, abs=0.1)
|
|
await asyncio.sleep(0.2)
|
|
assert any("on_control target_humidity=45.0" in line for line in log_lines)
|
|
|
|
# The device's next report is authoritative and overrides whatever was optimistically
|
|
# applied above -- this is the whole point of climate.template.publish: a device that owns
|
|
# its own state (e.g. changed by a physical remote) always wins.
|
|
client.button_command(report_button.key)
|
|
state = await wait_for_climate_state()
|
|
assert state.target_temperature_low == pytest.approx(18.0, abs=0.1)
|
|
assert state.target_temperature_high == pytest.approx(24.0, abs=0.1)
|
|
assert state.target_humidity == pytest.approx(50.0, abs=0.1)
|