core/tests/components/climate/test_init.py

722 lines
22 KiB
Python

"""The tests for the climate component."""
from __future__ import annotations
from enum import Enum
from typing import Any
from unittest.mock import MagicMock, Mock
import pytest
import voluptuous as vol
from homeassistant.components.climate import (
DOMAIN,
SET_TEMPERATURE_SCHEMA,
ClimateEntity,
HVACMode,
)
from homeassistant.components.climate.const import (
ATTR_CURRENT_TEMPERATURE,
ATTR_FAN_MODE,
ATTR_HUMIDITY,
ATTR_MAX_TEMP,
ATTR_MIN_TEMP,
ATTR_PRESET_MODE,
ATTR_SWING_HORIZONTAL_MODE,
ATTR_SWING_MODE,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
SERVICE_SET_FAN_MODE,
SERVICE_SET_HUMIDITY,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_PRESET_MODE,
SERVICE_SET_SWING_HORIZONTAL_MODE,
SERVICE_SET_SWING_MODE,
SERVICE_SET_TEMPERATURE,
SWING_HORIZONTAL_OFF,
SWING_HORIZONTAL_ON,
ClimateEntityFeature,
)
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from tests.common import (
MockConfigEntry,
MockEntity,
async_mock_service,
setup_test_component_platform,
)
async def test_set_temp_schema_no_req(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test the set temperature schema with missing required data."""
domain = "climate"
service = "test_set_temperature"
schema = SET_TEMPERATURE_SCHEMA
calls = async_mock_service(hass, domain, service, schema)
data = {"hvac_mode": "off", "entity_id": ["climate.test_id"]}
with pytest.raises(vol.Invalid):
await hass.services.async_call(domain, service, data)
await hass.async_block_till_done()
assert len(calls) == 0
async def test_set_temp_schema(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test the set temperature schema with ok required data."""
domain = "climate"
service = "test_set_temperature"
schema = SET_TEMPERATURE_SCHEMA
calls = async_mock_service(hass, domain, service, schema)
data = {"temperature": 20.0, "hvac_mode": "heat", "entity_id": ["climate.test_id"]}
await hass.services.async_call(domain, service, data)
await hass.async_block_till_done()
assert len(calls) == 1
assert calls[-1].data == data
class MockClimateEntity(MockEntity, ClimateEntity):
"""Mock Climate device to use in tests."""
_attr_supported_features = (
ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.PRESET_MODE
| ClimateEntityFeature.SWING_MODE
| ClimateEntityFeature.SWING_HORIZONTAL_MODE
)
_attr_preset_mode = "home"
_attr_preset_modes = ["home", "away"]
_attr_fan_mode = "auto"
_attr_fan_modes = ["auto", "off"]
_attr_swing_mode = "auto"
_attr_swing_modes = ["auto", "off"]
_attr_swing_horizontal_mode = "on"
_attr_swing_horizontal_modes = [SWING_HORIZONTAL_ON, SWING_HORIZONTAL_OFF]
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_target_temperature = 20
_attr_target_temperature_high = 25
_attr_target_temperature_low = 15
@property
def hvac_mode(self) -> HVACMode:
"""Return hvac operation ie. heat, cool mode.
Need to be one of HVACMode.*.
"""
return HVACMode.HEAT
@property
def hvac_modes(self) -> list[HVACMode]:
"""Return the list of available hvac operation modes.
Need to be a subset of HVAC_MODES.
"""
return [HVACMode.OFF, HVACMode.HEAT]
def set_preset_mode(self, preset_mode: str) -> None:
"""Set preset mode."""
self._attr_preset_mode = preset_mode
def set_fan_mode(self, fan_mode: str) -> None:
"""Set fan mode."""
self._attr_fan_mode = fan_mode
def set_swing_mode(self, swing_mode: str) -> None:
"""Set swing mode."""
self._attr_swing_mode = swing_mode
def set_swing_horizontal_mode(self, swing_horizontal_mode: str) -> None:
"""Set horizontal swing mode."""
self._attr_swing_horizontal_mode = swing_horizontal_mode
def set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target hvac mode."""
self._attr_hvac_mode = hvac_mode
def set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
if ATTR_TEMPERATURE in kwargs:
self._attr_target_temperature = kwargs[ATTR_TEMPERATURE]
if ATTR_TARGET_TEMP_HIGH in kwargs:
self._attr_target_temperature_high = kwargs[ATTR_TARGET_TEMP_HIGH]
self._attr_target_temperature_low = kwargs[ATTR_TARGET_TEMP_LOW]
class MockClimateEntityTestMethods(MockClimateEntity):
"""Mock Climate device."""
def turn_on(self) -> None:
"""Turn on."""
def turn_off(self) -> None:
"""Turn off."""
async def test_sync_turn_on(hass: HomeAssistant) -> None:
"""Test if async turn_on calls sync turn_on."""
climate = MockClimateEntityTestMethods()
climate.hass = hass
climate.turn_on = MagicMock()
await climate.async_turn_on()
assert climate.turn_on.called
async def test_sync_turn_off(hass: HomeAssistant) -> None:
"""Test if async turn_off calls sync turn_off."""
climate = MockClimateEntityTestMethods()
climate.hass = hass
climate.turn_off = MagicMock()
await climate.async_turn_off()
assert climate.turn_off.called
def _create_tuples(enum: type[Enum], constant_prefix: str) -> list[tuple[Enum, str]]:
return [
(enum_field, constant_prefix)
for enum_field in enum
if enum_field
not in [
ClimateEntityFeature.TURN_ON,
ClimateEntityFeature.TURN_OFF,
ClimateEntityFeature.SWING_HORIZONTAL_MODE,
]
]
async def test_temperature_features_is_valid(
hass: HomeAssistant,
register_test_integration: MockConfigEntry,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test correct features for setting temperature."""
class MockClimateTempEntity(MockClimateEntity):
@property
def supported_features(self) -> int:
"""Return supported features."""
return ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
class MockClimateTempRangeEntity(MockClimateEntity):
@property
def supported_features(self) -> int:
"""Return supported features."""
return ClimateEntityFeature.TARGET_TEMPERATURE
climate_temp_entity = MockClimateTempEntity(
name="test", entity_id="climate.test_temp"
)
climate_temp_range_entity = MockClimateTempRangeEntity(
name="test", entity_id="climate.test_range"
)
setup_test_component_platform(
hass,
DOMAIN,
entities=[climate_temp_entity, climate_temp_range_entity],
from_config_entry=True,
)
await hass.config_entries.async_setup(register_test_integration.entry_id)
await hass.async_block_till_done()
with pytest.raises(
ServiceValidationError,
match="Set temperature action was used with the target temperature parameter but the entity does not support it",
):
await hass.services.async_call(
DOMAIN,
SERVICE_SET_TEMPERATURE,
{
"entity_id": "climate.test_temp",
"temperature": 20,
},
blocking=True,
)
with pytest.raises(
ServiceValidationError,
match="Set temperature action was used with the target temperature low/high parameter but the entity does not support it",
):
await hass.services.async_call(
DOMAIN,
SERVICE_SET_TEMPERATURE,
{
"entity_id": "climate.test_range",
"target_temp_low": 20,
"target_temp_high": 25,
},
blocking=True,
)
async def test_mode_validation(
hass: HomeAssistant,
register_test_integration: MockConfigEntry,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test mode validation for hvac_mode, fan, swing and preset."""
climate_entity = MockClimateEntity(name="test", entity_id="climate.test")
setup_test_component_platform(
hass, DOMAIN, entities=[climate_entity], from_config_entry=True
)
await hass.config_entries.async_setup(register_test_integration.entry_id)
await hass.async_block_till_done()
state = hass.states.get("climate.test")
assert state.state == "heat"
assert state.attributes.get(ATTR_PRESET_MODE) == "home"
assert state.attributes.get(ATTR_FAN_MODE) == "auto"
assert state.attributes.get(ATTR_SWING_MODE) == "auto"
assert state.attributes.get(ATTR_SWING_HORIZONTAL_MODE) == "on"
await hass.services.async_call(
DOMAIN,
SERVICE_SET_PRESET_MODE,
{
"entity_id": "climate.test",
"preset_mode": "away",
},
blocking=True,
)
await hass.services.async_call(
DOMAIN,
SERVICE_SET_SWING_MODE,
{
"entity_id": "climate.test",
"swing_mode": "off",
},
blocking=True,
)
await hass.services.async_call(
DOMAIN,
SERVICE_SET_SWING_HORIZONTAL_MODE,
{
"entity_id": "climate.test",
"swing_horizontal_mode": "off",
},
blocking=True,
)
await hass.services.async_call(
DOMAIN,
SERVICE_SET_FAN_MODE,
{
"entity_id": "climate.test",
"fan_mode": "off",
},
blocking=True,
)
state = hass.states.get("climate.test")
assert state.attributes.get(ATTR_PRESET_MODE) == "away"
assert state.attributes.get(ATTR_FAN_MODE) == "off"
assert state.attributes.get(ATTR_SWING_MODE) == "off"
assert state.attributes.get(ATTR_SWING_HORIZONTAL_MODE) == "off"
with pytest.raises(
ServiceValidationError,
match="HVAC mode auto is not valid. Valid HVAC modes are: off, heat",
) as exc:
await hass.services.async_call(
DOMAIN,
SERVICE_SET_HVAC_MODE,
{
"entity_id": "climate.test",
"hvac_mode": "auto",
},
blocking=True,
)
assert (
str(exc.value) == "HVAC mode auto is not valid. Valid HVAC modes are: off, heat"
)
assert exc.value.translation_key == "not_valid_hvac_mode"
with pytest.raises(
ServiceValidationError,
match="Preset mode invalid is not valid. Valid preset modes are: home, away",
) as exc:
await hass.services.async_call(
DOMAIN,
SERVICE_SET_PRESET_MODE,
{
"entity_id": "climate.test",
"preset_mode": "invalid",
},
blocking=True,
)
assert (
str(exc.value)
== "Preset mode invalid is not valid. Valid preset modes are: home, away"
)
assert exc.value.translation_key == "not_valid_preset_mode"
with pytest.raises(
ServiceValidationError,
match="Swing mode invalid is not valid. Valid swing modes are: auto, off",
) as exc:
await hass.services.async_call(
DOMAIN,
SERVICE_SET_SWING_MODE,
{
"entity_id": "climate.test",
"swing_mode": "invalid",
},
blocking=True,
)
assert (
str(exc.value)
== "Swing mode invalid is not valid. Valid swing modes are: auto, off"
)
assert exc.value.translation_key == "not_valid_swing_mode"
with pytest.raises(
ServiceValidationError,
match="Horizontal swing mode invalid is not valid. Valid horizontal swing modes are: on, off",
) as exc:
await hass.services.async_call(
DOMAIN,
SERVICE_SET_SWING_HORIZONTAL_MODE,
{
"entity_id": "climate.test",
"swing_horizontal_mode": "invalid",
},
blocking=True,
)
assert (
str(exc.value)
== "Horizontal swing mode invalid is not valid. Valid horizontal swing modes are: on, off"
)
assert exc.value.translation_key == "not_valid_horizontal_swing_mode"
with pytest.raises(
ServiceValidationError,
match="Fan mode invalid is not valid. Valid fan modes are: auto, off",
) as exc:
await hass.services.async_call(
DOMAIN,
SERVICE_SET_FAN_MODE,
{
"entity_id": "climate.test",
"fan_mode": "invalid",
},
blocking=True,
)
assert (
str(exc.value)
== "Fan mode invalid is not valid. Valid fan modes are: auto, off"
)
assert exc.value.translation_key == "not_valid_fan_mode"
async def test_turn_on_off_toggle(hass: HomeAssistant) -> None:
"""Test turn_on/turn_off/toggle methods."""
class MockClimateEntityTest(MockClimateEntity):
"""Mock Climate device."""
_attr_hvac_mode = HVACMode.OFF
@property
def hvac_mode(self) -> HVACMode:
"""Return hvac mode."""
return self._attr_hvac_mode
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target hvac mode."""
self._attr_hvac_mode = hvac_mode
climate = MockClimateEntityTest()
climate.hass = hass
await climate.async_turn_on()
assert climate.hvac_mode == HVACMode.HEAT
await climate.async_turn_off()
assert climate.hvac_mode == HVACMode.OFF
await climate.async_toggle()
assert climate.hvac_mode == HVACMode.HEAT
await climate.async_toggle()
assert climate.hvac_mode == HVACMode.OFF
async def test_sync_toggle(hass: HomeAssistant) -> None:
"""Test if async toggle calls sync toggle."""
class MockClimateEntityTest(MockClimateEntity):
"""Mock Climate device."""
_attr_supported_features = (
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
)
@property
def hvac_mode(self) -> HVACMode:
"""Return hvac operation ie. heat, cool mode.
Need to be one of HVACMode.*.
"""
return HVACMode.HEAT
@property
def hvac_modes(self) -> list[HVACMode]:
"""Return the list of available hvac operation modes.
Need to be a subset of HVAC_MODES.
"""
return [HVACMode.OFF, HVACMode.HEAT]
def turn_on(self) -> None:
"""Turn on."""
def turn_off(self) -> None:
"""Turn off."""
def toggle(self) -> None:
"""Toggle."""
climate = MockClimateEntityTest()
climate.hass = hass
climate.toggle = Mock()
await climate.async_toggle()
assert climate.toggle.called
async def test_humidity_validation(
hass: HomeAssistant,
register_test_integration: MockConfigEntry,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test validation for humidity."""
class MockClimateEntityHumidity(MockClimateEntity):
"""Mock climate class with mocked aux heater."""
_attr_supported_features = ClimateEntityFeature.TARGET_HUMIDITY
_attr_target_humidity = 50
_attr_min_humidity = 50
_attr_max_humidity = 60
def set_humidity(self, humidity: int) -> None:
"""Set new target humidity."""
self._attr_target_humidity = humidity
test_climate = MockClimateEntityHumidity(
name="Test",
unique_id="unique_climate_test",
)
setup_test_component_platform(
hass, DOMAIN, entities=[test_climate], from_config_entry=True
)
await hass.config_entries.async_setup(register_test_integration.entry_id)
await hass.async_block_till_done()
state = hass.states.get("climate.test")
assert state.attributes.get(ATTR_HUMIDITY) == 50
with pytest.raises(
ServiceValidationError,
match="Provided humidity 1 is not valid. Accepted range is 50 to 60",
) as exc:
await hass.services.async_call(
DOMAIN,
SERVICE_SET_HUMIDITY,
{
"entity_id": "climate.test",
ATTR_HUMIDITY: "1",
},
blocking=True,
)
assert exc.value.translation_key == "humidity_out_of_range"
assert "Check valid humidity 1 in range 50 - 60" in caplog.text
with pytest.raises(
ServiceValidationError,
match="Provided humidity 70 is not valid. Accepted range is 50 to 60",
) as exc:
await hass.services.async_call(
DOMAIN,
SERVICE_SET_HUMIDITY,
{
"entity_id": "climate.test",
ATTR_HUMIDITY: "70",
},
blocking=True,
)
async def test_temperature_validation(
hass: HomeAssistant, register_test_integration: MockConfigEntry
) -> None:
"""Test validation for temperatures."""
class MockClimateEntityTemp(MockClimateEntity):
"""Mock climate class with mocked aux heater."""
_attr_supported_features = (
ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.PRESET_MODE
| ClimateEntityFeature.SWING_MODE
| ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
)
_attr_target_temperature = 15
_attr_target_temperature_high = 18
_attr_target_temperature_low = 10
_attr_target_temperature_step = PRECISION_WHOLE
def set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
if ATTR_TEMPERATURE in kwargs:
self._attr_target_temperature = kwargs[ATTR_TEMPERATURE]
if ATTR_TARGET_TEMP_HIGH in kwargs:
self._attr_target_temperature_high = kwargs[ATTR_TARGET_TEMP_HIGH]
self._attr_target_temperature_low = kwargs[ATTR_TARGET_TEMP_LOW]
test_climate = MockClimateEntityTemp(
name="Test",
unique_id="unique_climate_test",
)
setup_test_component_platform(
hass, DOMAIN, entities=[test_climate], from_config_entry=True
)
await hass.config_entries.async_setup(register_test_integration.entry_id)
await hass.async_block_till_done()
state = hass.states.get("climate.test")
assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) is None
assert state.attributes.get(ATTR_MIN_TEMP) == 7
assert state.attributes.get(ATTR_MAX_TEMP) == 35
with pytest.raises(
ServiceValidationError,
match="Provided temperature 40.0 is not valid. Accepted range is 7 to 35",
) as exc:
await hass.services.async_call(
DOMAIN,
SERVICE_SET_TEMPERATURE,
{
"entity_id": "climate.test",
ATTR_TEMPERATURE: "40",
},
blocking=True,
)
assert (
str(exc.value)
== "Provided temperature 40.0 is not valid. Accepted range is 7 to 35"
)
assert exc.value.translation_key == "temp_out_of_range"
with pytest.raises(
ServiceValidationError,
match="Provided temperature 0.0 is not valid. Accepted range is 7 to 35",
) as exc:
await hass.services.async_call(
DOMAIN,
SERVICE_SET_TEMPERATURE,
{
"entity_id": "climate.test",
ATTR_TARGET_TEMP_HIGH: "25",
ATTR_TARGET_TEMP_LOW: "0",
},
blocking=True,
)
assert (
str(exc.value)
== "Provided temperature 0.0 is not valid. Accepted range is 7 to 35"
)
assert exc.value.translation_key == "temp_out_of_range"
await hass.services.async_call(
DOMAIN,
SERVICE_SET_TEMPERATURE,
{
"entity_id": "climate.test",
ATTR_TARGET_TEMP_HIGH: "25",
ATTR_TARGET_TEMP_LOW: "10",
},
blocking=True,
)
state = hass.states.get("climate.test")
assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 10
assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 25
async def test_target_temp_high_higher_than_low(
hass: HomeAssistant, register_test_integration: MockConfigEntry
) -> None:
"""Test that target high is higher than target low."""
class MockClimateEntityTemp(MockClimateEntity):
"""Mock climate class with mocked aux heater."""
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
)
_attr_current_temperature = 15
_attr_target_temperature = 15
_attr_target_temperature_high = 18
_attr_target_temperature_low = 10
_attr_target_temperature_step = PRECISION_WHOLE
def set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
if ATTR_TEMPERATURE in kwargs:
self._attr_target_temperature = kwargs[ATTR_TEMPERATURE]
if ATTR_TARGET_TEMP_HIGH in kwargs:
self._attr_target_temperature_high = kwargs[ATTR_TARGET_TEMP_HIGH]
self._attr_target_temperature_low = kwargs[ATTR_TARGET_TEMP_LOW]
test_climate = MockClimateEntityTemp(
name="Test",
unique_id="unique_climate_test",
)
setup_test_component_platform(
hass, DOMAIN, entities=[test_climate], from_config_entry=True
)
await hass.config_entries.async_setup(register_test_integration.entry_id)
await hass.async_block_till_done()
state = hass.states.get("climate.test")
assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 15
assert state.attributes.get(ATTR_MIN_TEMP) == 7
assert state.attributes.get(ATTR_MAX_TEMP) == 35
with pytest.raises(
ServiceValidationError,
match="Target temperature low can not be higher than Target temperature high",
) as exc:
await hass.services.async_call(
DOMAIN,
SERVICE_SET_TEMPERATURE,
{
"entity_id": "climate.test",
ATTR_TARGET_TEMP_HIGH: "15",
ATTR_TARGET_TEMP_LOW: "20",
},
blocking=True,
)
assert (
str(exc.value)
== "Target temperature low can not be higher than Target temperature high"
)
assert exc.value.translation_key == "low_temp_higher_than_high_temp"