Files
core/tests/components/esphome/test_climate.py
Keith Burzinski 7af3eb638b [esphome] Implement feature_flags for climate (#153507)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-18 11:47:11 -10:00

688 lines
21 KiB
Python

"""Test ESPHome climates."""
import math
from unittest.mock import call
from aioesphomeapi import (
APIClient,
ClimateAction,
ClimateFanMode,
ClimateFeature,
ClimateInfo,
ClimateMode,
ClimatePreset,
ClimateState,
ClimateSwingMode,
)
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.climate import (
ATTR_CURRENT_HUMIDITY,
ATTR_CURRENT_TEMPERATURE,
ATTR_FAN_MODE,
ATTR_HUMIDITY,
ATTR_HVAC_MODE,
ATTR_MAX_HUMIDITY,
ATTR_MIN_HUMIDITY,
ATTR_PRESET_MODE,
ATTR_SWING_MODE,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
ATTR_TEMPERATURE,
DOMAIN as CLIMATE_DOMAIN,
FAN_HIGH,
SERVICE_SET_FAN_MODE,
SERVICE_SET_HUMIDITY,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_PRESET_MODE,
SERVICE_SET_SWING_MODE,
SERVICE_SET_TEMPERATURE,
SWING_BOTH,
HVACMode,
)
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from .conftest import MockGenericDeviceEntryType
async def test_climate_entity(
hass: HomeAssistant,
mock_client: APIClient,
mock_generic_device_entry: MockGenericDeviceEntryType,
) -> None:
"""Test a generic climate entity."""
entity_info = [
ClimateInfo(
object_id="myclimate",
key=1,
name="my climate",
supports_current_temperature=True,
supports_action=True,
visual_min_temperature=10.0,
visual_max_temperature=30.0,
)
]
states = [
ClimateState(
key=1,
mode=ClimateMode.COOL,
action=ClimateAction.COOLING,
current_temperature=30,
target_temperature=20,
fan_mode=ClimateFanMode.AUTO,
swing_mode=ClimateSwingMode.BOTH,
)
]
await mock_generic_device_entry(
mock_client=mock_client,
entity_info=entity_info,
states=states,
)
state = hass.states.get("climate.test_my_climate")
assert state is not None
assert state.state == HVACMode.COOL
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_TEMPERATURE: 25},
blocking=True,
)
mock_client.climate_command.assert_has_calls(
[call(key=1, target_temperature=25.0, device_id=0)]
)
mock_client.climate_command.reset_mock()
async def test_climate_entity_with_step_and_two_point(
hass: HomeAssistant,
mock_client: APIClient,
mock_generic_device_entry: MockGenericDeviceEntryType,
) -> None:
"""Test a generic climate entity."""
entity_info = [
ClimateInfo(
object_id="myclimate",
key=1,
name="my climate",
feature_flags=ClimateFeature.SUPPORTS_CURRENT_TEMPERATURE
| ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE,
visual_target_temperature_step=2,
visual_current_temperature_step=2,
visual_min_temperature=10.0,
visual_max_temperature=30.0,
supported_modes=[ClimateMode.COOL, ClimateMode.HEAT, ClimateMode.AUTO],
supported_presets=[ClimatePreset.AWAY, ClimatePreset.ACTIVITY],
)
]
states = [
ClimateState(
key=1,
mode=ClimateMode.COOL,
current_temperature=30,
target_temperature=20,
fan_mode=ClimateFanMode.AUTO,
swing_mode=ClimateSwingMode.BOTH,
)
]
await mock_generic_device_entry(
mock_client=mock_client,
entity_info=entity_info,
states=states,
)
state = hass.states.get("climate.test_my_climate")
assert state is not None
assert state.state == HVACMode.COOL
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_TEMPERATURE: 25},
blocking=True,
)
mock_client.climate_command.assert_has_calls(
[
call(
key=1,
target_temperature_high=25.0,
device_id=0,
)
]
)
mock_client.climate_command.reset_mock()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{
ATTR_ENTITY_ID: "climate.test_my_climate",
ATTR_HVAC_MODE: HVACMode.AUTO,
ATTR_TARGET_TEMP_LOW: 20,
ATTR_TARGET_TEMP_HIGH: 30,
},
blocking=True,
)
mock_client.climate_command.assert_has_calls(
[
call(
key=1,
mode=ClimateMode.AUTO,
target_temperature_low=20.0,
target_temperature_high=30.0,
device_id=0,
)
]
)
mock_client.climate_command.reset_mock()
async def test_climate_entity_with_step_and_target_temp(
hass: HomeAssistant,
mock_client: APIClient,
mock_generic_device_entry: MockGenericDeviceEntryType,
) -> None:
"""Test a generic climate entity."""
entity_info = [
ClimateInfo(
object_id="myclimate",
key=1,
name="my climate",
supports_current_temperature=True,
visual_target_temperature_step=2,
visual_current_temperature_step=2,
supports_action=True,
visual_min_temperature=10.0,
visual_max_temperature=30.0,
supported_fan_modes=[ClimateFanMode.LOW, ClimateFanMode.HIGH],
supported_modes=[ClimateMode.COOL, ClimateMode.HEAT, ClimateMode.AUTO],
supported_presets=[ClimatePreset.AWAY, ClimatePreset.ACTIVITY],
supported_custom_presets=["preset1", "preset2"],
supported_custom_fan_modes=["fan1", "fan2"],
supported_swing_modes=[ClimateSwingMode.BOTH, ClimateSwingMode.OFF],
)
]
states = [
ClimateState(
key=1,
mode=ClimateMode.COOL,
action=ClimateAction.COOLING,
current_temperature=30,
target_temperature=20,
fan_mode=ClimateFanMode.AUTO,
swing_mode=ClimateSwingMode.BOTH,
)
]
await mock_generic_device_entry(
mock_client=mock_client,
entity_info=entity_info,
states=states,
)
state = hass.states.get("climate.test_my_climate")
assert state is not None
assert state.state == HVACMode.COOL
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{
ATTR_ENTITY_ID: "climate.test_my_climate",
ATTR_HVAC_MODE: HVACMode.AUTO,
ATTR_TEMPERATURE: 25,
},
blocking=True,
)
mock_client.climate_command.assert_has_calls(
[call(key=1, mode=ClimateMode.AUTO, target_temperature=25.0, device_id=0)]
)
mock_client.climate_command.reset_mock()
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{
ATTR_ENTITY_ID: "climate.test_my_climate",
ATTR_HVAC_MODE: HVACMode.AUTO,
ATTR_TARGET_TEMP_LOW: 20,
ATTR_TARGET_TEMP_HIGH: 30,
},
blocking=True,
)
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{
ATTR_ENTITY_ID: "climate.test_my_climate",
ATTR_HVAC_MODE: HVACMode.HEAT,
},
blocking=True,
)
mock_client.climate_command.assert_has_calls(
[
call(
key=1,
mode=ClimateMode.HEAT,
device_id=0,
)
]
)
mock_client.climate_command.reset_mock()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_PRESET_MODE,
{ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_PRESET_MODE: "away"},
blocking=True,
)
mock_client.climate_command.assert_has_calls(
[
call(
key=1,
preset=ClimatePreset.AWAY,
device_id=0,
)
]
)
mock_client.climate_command.reset_mock()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_PRESET_MODE,
{ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_PRESET_MODE: "preset1"},
blocking=True,
)
mock_client.climate_command.assert_has_calls(
[call(key=1, custom_preset="preset1", device_id=0)]
)
mock_client.climate_command.reset_mock()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
{ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_FAN_MODE: FAN_HIGH},
blocking=True,
)
mock_client.climate_command.assert_has_calls(
[call(key=1, fan_mode=ClimateFanMode.HIGH, device_id=0)]
)
mock_client.climate_command.reset_mock()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
{ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_FAN_MODE: "fan2"},
blocking=True,
)
mock_client.climate_command.assert_has_calls(
[call(key=1, custom_fan_mode="fan2", device_id=0)]
)
mock_client.climate_command.reset_mock()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_SWING_MODE,
{ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_SWING_MODE: SWING_BOTH},
blocking=True,
)
mock_client.climate_command.assert_has_calls(
[call(key=1, swing_mode=ClimateSwingMode.BOTH, device_id=0)]
)
mock_client.climate_command.reset_mock()
async def test_climate_entity_with_humidity(
hass: HomeAssistant,
mock_client: APIClient,
mock_generic_device_entry: MockGenericDeviceEntryType,
) -> None:
"""Test a generic climate entity with humidity."""
entity_info = [
ClimateInfo(
object_id="myclimate",
key=1,
name="my climate",
feature_flags=ClimateFeature.SUPPORTS_CURRENT_TEMPERATURE
| ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE
| ClimateFeature.SUPPORTS_CURRENT_HUMIDITY
| ClimateFeature.SUPPORTS_TARGET_HUMIDITY
| ClimateFeature.SUPPORTS_ACTION,
visual_min_temperature=10.0,
visual_max_temperature=30.0,
visual_min_humidity=10.1,
visual_max_humidity=29.7,
)
]
states = [
ClimateState(
key=1,
mode=ClimateMode.AUTO,
action=ClimateAction.COOLING,
current_temperature=30,
target_temperature=20,
fan_mode=ClimateFanMode.AUTO,
swing_mode=ClimateSwingMode.BOTH,
current_humidity=20.1,
target_humidity=25.7,
)
]
await mock_generic_device_entry(
mock_client=mock_client,
entity_info=entity_info,
states=states,
)
state = hass.states.get("climate.test_my_climate")
assert state is not None
assert state.state == HVACMode.AUTO
attributes = state.attributes
assert attributes[ATTR_CURRENT_HUMIDITY] == 20
assert attributes[ATTR_HUMIDITY] == 26
assert attributes[ATTR_MAX_HUMIDITY] == 30
assert attributes[ATTR_MIN_HUMIDITY] == 10
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HUMIDITY,
{ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_HUMIDITY: 23},
blocking=True,
)
mock_client.climate_command.assert_has_calls(
[call(key=1, target_humidity=23, device_id=0)]
)
mock_client.climate_command.reset_mock()
async def test_climate_entity_with_heat(
hass: HomeAssistant,
mock_client: APIClient,
mock_generic_device_entry: MockGenericDeviceEntryType,
) -> None:
"""Test a generic climate entity with heat."""
entity_info = [
ClimateInfo(
object_id="myclimate",
key=1,
name="my climate",
feature_flags=ClimateFeature.SUPPORTS_CURRENT_TEMPERATURE
| ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE
| ClimateFeature.SUPPORTS_ACTION,
visual_min_temperature=10.0,
visual_max_temperature=30.0,
supported_modes=[ClimateMode.COOL, ClimateMode.HEAT, ClimateMode.AUTO],
)
]
states = [
ClimateState(
key=1,
mode=ClimateMode.HEAT,
action=ClimateAction.HEATING,
current_temperature=18,
target_temperature=22,
)
]
await mock_generic_device_entry(
mock_client=mock_client,
entity_info=entity_info,
states=states,
)
state = hass.states.get("climate.test_my_climate")
assert state is not None
assert state.state == HVACMode.HEAT
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_TEMPERATURE: 23},
blocking=True,
)
mock_client.climate_command.assert_has_calls(
[call(key=1, target_temperature_low=23, device_id=0)]
)
mock_client.climate_command.reset_mock()
async def test_climate_entity_with_heat_cool(
hass: HomeAssistant,
mock_client: APIClient,
mock_generic_device_entry: MockGenericDeviceEntryType,
) -> None:
"""Test a generic climate entity with heat."""
entity_info = [
ClimateInfo(
object_id="myclimate",
key=1,
name="my climate",
feature_flags=ClimateFeature.SUPPORTS_CURRENT_TEMPERATURE
| ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE
| ClimateFeature.SUPPORTS_ACTION,
visual_min_temperature=10.0,
visual_max_temperature=30.0,
supported_modes=[ClimateMode.COOL, ClimateMode.HEAT, ClimateMode.HEAT_COOL],
)
]
states = [
ClimateState(
key=1,
mode=ClimateMode.HEAT_COOL,
action=ClimateAction.HEATING,
current_temperature=18,
target_temperature=22,
)
]
await mock_generic_device_entry(
mock_client=mock_client,
entity_info=entity_info,
states=states,
)
state = hass.states.get("climate.test_my_climate")
assert state is not None
assert state.state == HVACMode.HEAT_COOL
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{
ATTR_ENTITY_ID: "climate.test_my_climate",
ATTR_TARGET_TEMP_HIGH: 23,
ATTR_TARGET_TEMP_LOW: 20,
},
blocking=True,
)
mock_client.climate_command.assert_has_calls(
[
call(
key=1,
target_temperature_high=23,
target_temperature_low=20,
device_id=0,
)
]
)
mock_client.climate_command.reset_mock()
async def test_climate_set_temperature_unsupported_mode(
hass: HomeAssistant,
mock_client: APIClient,
mock_generic_device_entry: MockGenericDeviceEntryType,
) -> None:
"""Test setting temperature in unsupported mode with two-point temperature support."""
entity_info = [
ClimateInfo(
object_id="myclimate",
key=1,
name="my climate",
feature_flags=ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE,
supported_modes=[ClimateMode.HEAT, ClimateMode.COOL, ClimateMode.AUTO],
visual_min_temperature=10.0,
visual_max_temperature=30.0,
)
]
states = [
ClimateState(
key=1,
mode=ClimateMode.AUTO,
target_temperature=20,
)
]
await mock_generic_device_entry(
mock_client=mock_client,
entity_info=entity_info,
states=states,
)
with pytest.raises(
ServiceValidationError,
match="Setting target_temperature is only supported in heat or cool modes",
):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{
ATTR_ENTITY_ID: "climate.test_my_climate",
ATTR_TEMPERATURE: 25,
},
blocking=True,
)
mock_client.climate_command.assert_not_called()
async def test_climate_entity_with_inf_value(
hass: HomeAssistant,
mock_client: APIClient,
mock_generic_device_entry: MockGenericDeviceEntryType,
) -> None:
"""Test a generic climate entity with infinite temp."""
entity_info = [
ClimateInfo(
object_id="myclimate",
key=1,
name="my climate",
feature_flags=ClimateFeature.SUPPORTS_CURRENT_TEMPERATURE
| ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE
| ClimateFeature.SUPPORTS_CURRENT_HUMIDITY
| ClimateFeature.SUPPORTS_TARGET_HUMIDITY
| ClimateFeature.SUPPORTS_ACTION,
visual_min_temperature=10.0,
visual_max_temperature=30.0,
visual_min_humidity=10.1,
visual_max_humidity=29.7,
)
]
states = [
ClimateState(
key=1,
mode=ClimateMode.AUTO,
action=ClimateAction.COOLING,
current_temperature=math.inf,
target_temperature=math.inf,
fan_mode=ClimateFanMode.AUTO,
swing_mode=ClimateSwingMode.BOTH,
current_humidity=math.inf,
target_humidity=25.7,
)
]
await mock_generic_device_entry(
mock_client=mock_client,
entity_info=entity_info,
states=states,
)
state = hass.states.get("climate.test_my_climate")
assert state is not None
assert state.state == HVACMode.AUTO
attributes = state.attributes
assert ATTR_CURRENT_HUMIDITY not in attributes
assert attributes[ATTR_HUMIDITY] == 26
assert attributes[ATTR_MAX_HUMIDITY] == 30
assert attributes[ATTR_MIN_HUMIDITY] == 10
assert attributes[ATTR_TEMPERATURE] is None
assert attributes[ATTR_CURRENT_TEMPERATURE] is None
async def test_climate_entity_attributes(
hass: HomeAssistant,
mock_client: APIClient,
mock_generic_device_entry: MockGenericDeviceEntryType,
snapshot: SnapshotAssertion,
) -> None:
"""Test a climate entity sets correct attributes."""
entity_info = [
ClimateInfo(
object_id="myclimate",
key=1,
name="my climate",
feature_flags=ClimateFeature.SUPPORTS_CURRENT_TEMPERATURE
| ClimateFeature.SUPPORTS_ACTION,
visual_target_temperature_step=2,
visual_current_temperature_step=2,
visual_min_temperature=10.0,
visual_max_temperature=30.0,
supported_fan_modes=[ClimateFanMode.LOW, ClimateFanMode.HIGH],
supported_modes=[
ClimateMode.COOL,
ClimateMode.HEAT,
ClimateMode.AUTO,
ClimateMode.OFF,
],
supported_presets=[ClimatePreset.AWAY, ClimatePreset.ACTIVITY],
supported_custom_presets=["preset1", "preset2"],
supported_custom_fan_modes=["fan1", "fan2"],
supported_swing_modes=[ClimateSwingMode.BOTH, ClimateSwingMode.OFF],
)
]
states = [
ClimateState(
key=1,
mode=ClimateMode.COOL,
action=ClimateAction.COOLING,
current_temperature=30,
target_temperature=20,
fan_mode=ClimateFanMode.AUTO,
swing_mode=ClimateSwingMode.BOTH,
)
]
await mock_generic_device_entry(
mock_client=mock_client,
entity_info=entity_info,
states=states,
)
state = hass.states.get("climate.test_my_climate")
assert state is not None
assert state.state == HVACMode.COOL
assert state.attributes == snapshot(name="climate-entity-attributes")
async def test_climate_entity_attribute_current_temperature_unsupported(
hass: HomeAssistant,
mock_client: APIClient,
mock_generic_device_entry: MockGenericDeviceEntryType,
) -> None:
"""Test a climate entity with current temperature unsupported."""
entity_info = [
ClimateInfo(
object_id="myclimate",
key=1,
name="my climate",
supports_current_temperature=False,
)
]
states = [
ClimateState(
key=1,
current_temperature=30,
)
]
await mock_generic_device_entry(
mock_client=mock_client,
entity_info=entity_info,
states=states,
)
state = hass.states.get("climate.test_my_climate")
assert state is not None
assert state.attributes[ATTR_CURRENT_TEMPERATURE] is None