mirror of
https://github.com/home-assistant/core.git
synced 2026-09-22 17:52:44 +01:00
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2452 lines
70 KiB
Python
2452 lines
70 KiB
Python
"""The tests for the Template climate platform."""
|
|
|
|
from enum import StrEnum
|
|
from itertools import chain
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from syrupy.assertion import SnapshotAssertion
|
|
|
|
from homeassistant.components import climate, template
|
|
from homeassistant.components.climate import (
|
|
ClimateEntityCapabilityAttribute,
|
|
ClimateEntityFeature,
|
|
ClimateEntityStateAttribute,
|
|
HVACAction,
|
|
HVACMode,
|
|
)
|
|
from homeassistant.components.template.climate import DEFAULT_NAME
|
|
from homeassistant.const import (
|
|
ATTR_ENTITY_ID,
|
|
STATE_OFF,
|
|
STATE_ON,
|
|
STATE_UNAVAILABLE,
|
|
STATE_UNKNOWN,
|
|
UnitOfTemperature,
|
|
)
|
|
from homeassistant.core import HomeAssistant, ServiceCall
|
|
from homeassistant.exceptions import ServiceValidationError
|
|
from homeassistant.helpers import entity_registry as er
|
|
from homeassistant.helpers.restore_state import STORAGE_KEY as RESTORE_STATE_KEY
|
|
from homeassistant.helpers.typing import ConfigType
|
|
|
|
from .conftest import (
|
|
ConfigurationStyle,
|
|
TemplatePlatformSetup,
|
|
assert_action,
|
|
assert_attributes_template,
|
|
assert_extra_template_attributes,
|
|
assert_invalid_config_entry_actions_do_not_create_entities,
|
|
assert_invalid_yaml_actions_do_not_create_entities,
|
|
assert_state_and_attributes,
|
|
async_get_flow_preview_state,
|
|
async_trigger,
|
|
make_test_action,
|
|
make_test_trigger,
|
|
setup_and_test_nested_unique_id,
|
|
setup_and_test_unique_id,
|
|
setup_entity,
|
|
setup_mock_template_entity_restore_state,
|
|
setup_restore_template_entity,
|
|
)
|
|
|
|
from tests.common import MockConfigEntry, async_mock_restore_state_shutdown_restart
|
|
from tests.typing import WebSocketGenerator
|
|
|
|
TEST_STATE_ENTITY_ID = "sensor.test_state"
|
|
TEST_ATTRIBUTE_ENTITY_ID = "sensor.test_attribute"
|
|
TEST_AVAILABILITY_ENTITY = "binary_sensor.availability"
|
|
|
|
TEST_CLIMATE = TemplatePlatformSetup(
|
|
climate.DOMAIN,
|
|
"test_climate",
|
|
make_test_trigger(
|
|
TEST_STATE_ENTITY_ID,
|
|
TEST_AVAILABILITY_ENTITY,
|
|
TEST_ATTRIBUTE_ENTITY_ID,
|
|
),
|
|
)
|
|
|
|
SET_FAN_MODE_ACTION = make_test_action(
|
|
"set_fan_mode",
|
|
{
|
|
"fan_mode": "{{ fan_mode }}",
|
|
},
|
|
)
|
|
SET_HUMIDITY_ACTION = make_test_action(
|
|
"set_humidity",
|
|
{
|
|
"humidity": "{{ humidity }}",
|
|
},
|
|
)
|
|
SET_HVAC_MODE_ACTION = make_test_action(
|
|
"set_hvac_mode",
|
|
{
|
|
"hvac_mode": "{{ hvac_mode }}",
|
|
},
|
|
)
|
|
SET_PRESET_MODE_ACTION = make_test_action(
|
|
"set_preset_mode",
|
|
{
|
|
"preset_mode": "{{ preset_mode }}",
|
|
},
|
|
)
|
|
SET_SWING_HORIZONTAL_MODE_ACTION = make_test_action(
|
|
"set_swing_horizontal_mode",
|
|
{
|
|
"swing_horizontal_mode": "{{ swing_horizontal_mode }}",
|
|
},
|
|
)
|
|
SET_SWING_MODE_ACTION = make_test_action(
|
|
"set_swing_mode",
|
|
{
|
|
"swing_mode": "{{ swing_mode }}",
|
|
},
|
|
)
|
|
SET_TEMPERATURE_ACTION = make_test_action(
|
|
"set_temperature",
|
|
{
|
|
"temperature": "{{ temperature }}",
|
|
"target_temp_high": "{{ target_temp_high }}",
|
|
"target_temp_low": "{{ target_temp_low }}",
|
|
"hvac_mode": "{{ hvac_mode }}",
|
|
},
|
|
)
|
|
|
|
HVAC_MODES = {"hvac_modes": "{{ ['off', 'heat', 'cool', 'heat_cool'] }}"}
|
|
EXPECTED_HVAC_MODES = [HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL, HVACMode.HEAT_COOL]
|
|
MINIMUM_REQUIREMENTS = {
|
|
**HVAC_MODES,
|
|
**SET_HVAC_MODE_ACTION,
|
|
}
|
|
|
|
|
|
async def _call_and_assert_action(
|
|
hass: HomeAssistant,
|
|
calls: list[ServiceCall],
|
|
service: str,
|
|
service_data: ConfigType | None = None,
|
|
expected_data: ConfigType | None = None,
|
|
expected_action: str | None = None,
|
|
) -> None:
|
|
"""Call a service and validate that it was called properly.
|
|
|
|
The service is validated when expected_action is omitted.
|
|
"""
|
|
if expected_action is None:
|
|
expected_action = service
|
|
current = len(calls)
|
|
await hass.services.async_call(
|
|
climate.DOMAIN,
|
|
service,
|
|
{**(service_data or {}), ATTR_ENTITY_ID: TEST_CLIMATE.entity_id},
|
|
blocking=True,
|
|
)
|
|
assert_action(
|
|
TEST_CLIMATE, calls, current + 1, expected_action, **(expected_data or {})
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
async def setup_base_climate(
|
|
hass: HomeAssistant,
|
|
count: int,
|
|
style: ConfigurationStyle,
|
|
config: dict[str, Any],
|
|
) -> None:
|
|
"""Do setup of climate integration."""
|
|
await setup_entity(hass, TEST_CLIMATE, style, count, config)
|
|
|
|
|
|
@pytest.fixture
|
|
async def setup_climate(
|
|
hass: HomeAssistant,
|
|
style: ConfigurationStyle,
|
|
config: dict[str, Any],
|
|
extra_config: dict[str, Any],
|
|
) -> None:
|
|
"""Do setup of climate integration."""
|
|
await setup_entity(hass, TEST_CLIMATE, style, 1, config, extra_config=extra_config)
|
|
|
|
|
|
@pytest.fixture
|
|
async def setup_single_attribute_climate(
|
|
hass: HomeAssistant,
|
|
style: ConfigurationStyle,
|
|
attribute: str,
|
|
attribute_template: str,
|
|
extra_config: dict,
|
|
) -> None:
|
|
"""Do setup of climate integration."""
|
|
await setup_entity(
|
|
hass,
|
|
TEST_CLIMATE,
|
|
style,
|
|
1,
|
|
{attribute: attribute_template} if attribute and attribute_template else {},
|
|
extra_config=extra_config,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("attribute", "extra_config"),
|
|
[("current_humidity", MINIMUM_REQUIREMENTS)],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("attribute_template", "expected"),
|
|
[
|
|
("{{ 20 }}", 20),
|
|
("{{ 30 }}", 30),
|
|
("{{ 45 }}", 45),
|
|
("{{ 99 }}", 99),
|
|
("{{ 100 }}", 100),
|
|
("{{ 45.5 }}", 45),
|
|
("{{ -1 }}", None),
|
|
("{{ 101 }}", None),
|
|
("{{ True }}", None),
|
|
("{{ False }}", None),
|
|
("{{ 'something' }}", None),
|
|
("{{ x - 1 }}", None),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_single_attribute_climate")
|
|
async def test_humidity_template(hass: HomeAssistant, expected: Any) -> None:
|
|
"""Test template humidity."""
|
|
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state.attributes.get("current_humidity") == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("config", "extra_config"),
|
|
[(MINIMUM_REQUIREMENTS, {"target_humidity": "{{ 65 }}", **SET_HUMIDITY_ACTION})],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.usefixtures("setup_climate")
|
|
async def test_set_humidity_action(
|
|
hass: HomeAssistant,
|
|
calls: list[ServiceCall],
|
|
) -> None:
|
|
"""Test set_humidity action."""
|
|
|
|
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state is not None
|
|
|
|
await _call_and_assert_action(
|
|
hass,
|
|
calls,
|
|
"set_humidity",
|
|
{"humidity": 45},
|
|
{"humidity": 45},
|
|
"set_humidity",
|
|
)
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state is not None
|
|
assert state.attributes["humidity"] == 65
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("config", "extra_config"), [(MINIMUM_REQUIREMENTS, SET_HUMIDITY_ACTION)]
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.usefixtures("setup_climate")
|
|
async def test_optimistic_set_humidity_action(
|
|
hass: HomeAssistant,
|
|
calls: list[ServiceCall],
|
|
) -> None:
|
|
"""Test optimistic set_humidity action."""
|
|
|
|
await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_ON)
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state is not None
|
|
|
|
await _call_and_assert_action(
|
|
hass,
|
|
calls,
|
|
"set_humidity",
|
|
{"humidity": 45},
|
|
{"humidity": 45},
|
|
"set_humidity",
|
|
)
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state is not None
|
|
assert state.attributes["humidity"] == 45
|
|
|
|
await _call_and_assert_action(
|
|
hass,
|
|
calls,
|
|
"set_humidity",
|
|
{"humidity": 65},
|
|
{"humidity": 65},
|
|
"set_humidity",
|
|
)
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state is not None
|
|
assert state.attributes["humidity"] == 65
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("attribute", "extra_config"),
|
|
[("current_temperature", MINIMUM_REQUIREMENTS)],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("attribute_template", "expected"),
|
|
[
|
|
("{{ -1 }}", -1),
|
|
("{{ 5.3423 }}", 5.3),
|
|
("{{ 30 }}", 30),
|
|
("{{ 45 }}", 45),
|
|
("{{ 99 }}", 99),
|
|
("{{ 100 }}", 100),
|
|
("{{ 45.5 }}", 45.5),
|
|
("{{ True }}", None),
|
|
("{{ False }}", None),
|
|
("{{ 'something' }}", None),
|
|
("{{ x - 1 }}", None),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_single_attribute_climate")
|
|
async def test_temperature_template(hass: HomeAssistant, expected: Any) -> None:
|
|
"""Test template temperature."""
|
|
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state.attributes.get("current_temperature") == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("attribute", "extra_config"),
|
|
[("hvac_action", MINIMUM_REQUIREMENTS)],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("attribute_template", "expected"),
|
|
[
|
|
("{{ 'cooling' }}", HVACAction.COOLING),
|
|
("{{ 'defrosting' }}", HVACAction.DEFROSTING),
|
|
("{{ 'drying' }}", HVACAction.DRYING),
|
|
("{{ 'fan' }}", HVACAction.FAN),
|
|
("{{ 'heating' }}", HVACAction.HEATING),
|
|
("{{ 'idle' }}", HVACAction.IDLE),
|
|
("{{ 'off' }}", HVACAction.OFF),
|
|
("{{ 'preheating' }}", HVACAction.PREHEATING),
|
|
("{{ 100 }}", None),
|
|
("{{ 45.5 }}", None),
|
|
("{{ True }}", None),
|
|
("{{ False }}", None),
|
|
("{{ 'something' }}", None),
|
|
("{{ x - 1 }}", None),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_single_attribute_climate")
|
|
async def test_hvac_action_template(hass: HomeAssistant, expected: Any) -> None:
|
|
"""Test template hvac_action."""
|
|
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state.attributes.get("hvac_action") == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("attribute", "extra_config"),
|
|
[
|
|
(
|
|
"target_humidity",
|
|
{
|
|
"min_humidity": 19,
|
|
"max_humidity": 100,
|
|
**SET_HUMIDITY_ACTION,
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
)
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("attribute_template", "expected"),
|
|
[
|
|
("{{ 20 }}", 20),
|
|
("{{ 30 }}", 30),
|
|
("{{ 45 }}", 45),
|
|
("{{ 99 }}", 99),
|
|
("{{ 100 }}", 100),
|
|
("{{ 45.5 }}", 45),
|
|
("{{ -1 }}", None),
|
|
("{{ 101 }}", None),
|
|
("{{ True }}", None),
|
|
("{{ False }}", None),
|
|
("{{ 'something' }}", None),
|
|
("{{ x - 1 }}", None),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_single_attribute_climate")
|
|
async def test_target_humidity_template(hass: HomeAssistant, expected: Any) -> None:
|
|
"""Test template target_humidity."""
|
|
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state.attributes.get("humidity") == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"config",
|
|
[
|
|
{"target_humidity": 32, **MINIMUM_REQUIREMENTS},
|
|
],
|
|
)
|
|
async def test_missing_set_humidity_config(
|
|
hass: HomeAssistant,
|
|
style: ConfigurationStyle,
|
|
config: ConfigType,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test a bad target humidity configuration."""
|
|
platform = TEST_CLIMATE
|
|
await setup_entity(hass, platform, style, 0, config)
|
|
assert len(hass.states.async_all(platform.domain)) == 0
|
|
assert (
|
|
"Invalid config for 'template': Required option: 'set_humidity' is missing for option"
|
|
in caplog.text
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("attribute", "extra_config"),
|
|
[
|
|
(
|
|
"target_temperature",
|
|
{
|
|
"min_temperature": -2,
|
|
"max_temperature": 101,
|
|
**SET_TEMPERATURE_ACTION,
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
)
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("attribute_template", "expected"),
|
|
[
|
|
("{{ -1 }}", -1),
|
|
("{{ 5.3423 }}", 5.3),
|
|
("{{ 30 }}", 30),
|
|
("{{ 45 }}", 45),
|
|
("{{ 99 }}", 99),
|
|
("{{ 100 }}", 100),
|
|
("{{ 45.5 }}", 45.5),
|
|
("{{ -3 }}", None),
|
|
("{{ 103 }}", None),
|
|
("{{ True }}", None),
|
|
("{{ False }}", None),
|
|
("{{ 'something' }}", None),
|
|
("{{ x - 1 }}", None),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_single_attribute_climate")
|
|
async def test_target_temperature_template(hass: HomeAssistant, expected: Any) -> None:
|
|
"""Test template target_temperature."""
|
|
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state.attributes.get("temperature") == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"config",
|
|
[{"hvac_mode": "{{ 'cool' }}", **MINIMUM_REQUIREMENTS, **SET_TEMPERATURE_ACTION}],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
(
|
|
"extra_config",
|
|
"action_variables",
|
|
"expected_variables",
|
|
"expected_attributes",
|
|
),
|
|
[
|
|
(
|
|
{
|
|
"target_temperature": "{{ 21 }}",
|
|
},
|
|
{"temperature": 18},
|
|
{"temperature": 18},
|
|
{"temperature": 21},
|
|
),
|
|
(
|
|
{
|
|
"target_temperature_low": "{{ 10 }}",
|
|
"target_temperature_high": "{{ 30 }}",
|
|
},
|
|
{"target_temp_low": 11, "target_temp_high": 29},
|
|
{"target_temp_low": 11, "target_temp_high": 29},
|
|
{"target_temp_low": 10, "target_temp_high": 30},
|
|
),
|
|
(
|
|
{
|
|
"target_temperature": "{{ 20 }}",
|
|
"target_temperature_low": "{{ 10 }}",
|
|
"target_temperature_high": "{{ 30 }}",
|
|
},
|
|
{"temperature": 21, "target_temp_low": 11, "target_temp_high": 29},
|
|
{"temperature": 21, "target_temp_low": 11, "target_temp_high": 29},
|
|
{"temperature": 20, "target_temp_low": 10, "target_temp_high": 30},
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.usefixtures("setup_climate")
|
|
async def test_set_temperature_action_with_hvac_mode(
|
|
hass: HomeAssistant,
|
|
action_variables: ConfigType,
|
|
expected_variables: ConfigType,
|
|
expected_attributes: ConfigType,
|
|
calls: list[ServiceCall],
|
|
) -> None:
|
|
"""Test set_temperature action."""
|
|
|
|
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state is not None
|
|
|
|
await hass.services.async_call(
|
|
climate.DOMAIN,
|
|
"set_temperature",
|
|
{
|
|
"hvac_mode": "heat",
|
|
**action_variables,
|
|
ATTR_ENTITY_ID: TEST_CLIMATE.entity_id,
|
|
},
|
|
blocking=True,
|
|
)
|
|
assert_action(TEST_CLIMATE, calls, 2, "set_hvac_mode", 0, hvac_mode="heat")
|
|
assert_action(
|
|
TEST_CLIMATE,
|
|
calls,
|
|
2,
|
|
"set_temperature",
|
|
hvac_mode="heat",
|
|
**expected_variables,
|
|
)
|
|
|
|
assert_state_and_attributes(hass, TEST_CLIMATE, HVACMode.COOL, expected_attributes)
|
|
|
|
with pytest.raises(ServiceValidationError):
|
|
await hass.services.async_call(
|
|
climate.DOMAIN,
|
|
"set_temperature",
|
|
{
|
|
"hvac_mode": "fan_only",
|
|
"temperature": 20,
|
|
ATTR_ENTITY_ID: TEST_CLIMATE.entity_id,
|
|
},
|
|
blocking=True,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("config", [{**MINIMUM_REQUIREMENTS, **SET_TEMPERATURE_ACTION}])
|
|
@pytest.mark.parametrize(
|
|
(
|
|
"extra_config",
|
|
"action_variables",
|
|
"expected_variables",
|
|
"expected_attributes",
|
|
),
|
|
[
|
|
(
|
|
{"target_temperature": "{{ 21 }}"},
|
|
{"temperature": 18},
|
|
{"temperature": 18},
|
|
{"temperature": 21},
|
|
),
|
|
(
|
|
{
|
|
"target_temperature_low": "{{ 10 }}",
|
|
"target_temperature_high": "{{ 30 }}",
|
|
},
|
|
{"target_temp_low": 11, "target_temp_high": 29},
|
|
{"target_temp_low": 11, "target_temp_high": 29},
|
|
{"target_temp_low": 10, "target_temp_high": 30},
|
|
),
|
|
(
|
|
{
|
|
"target_temperature": "{{ 20 }}",
|
|
"target_temperature_low": "{{ 10 }}",
|
|
"target_temperature_high": "{{ 30 }}",
|
|
},
|
|
{"temperature": 21, "target_temp_low": 11, "target_temp_high": 29},
|
|
{"temperature": 21, "target_temp_low": 11, "target_temp_high": 29},
|
|
{"temperature": 20, "target_temp_low": 10, "target_temp_high": 30},
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.usefixtures("setup_climate")
|
|
async def test_set_temperature_action_without_hvac_mode(
|
|
hass: HomeAssistant,
|
|
action_variables: ConfigType,
|
|
expected_variables: ConfigType,
|
|
expected_attributes: ConfigType,
|
|
calls: list[ServiceCall],
|
|
) -> None:
|
|
"""Test set_temperature action does not call set_hvac_mode action without hvac_mode."""
|
|
|
|
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state is not None
|
|
|
|
await _call_and_assert_action(
|
|
hass,
|
|
calls,
|
|
"set_temperature",
|
|
action_variables,
|
|
expected_variables,
|
|
"set_temperature",
|
|
)
|
|
|
|
assert_state_and_attributes(hass, TEST_CLIMATE, STATE_UNKNOWN, expected_attributes)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("config", "extra_config"), [(MINIMUM_REQUIREMENTS, SET_TEMPERATURE_ACTION)]
|
|
)
|
|
@pytest.mark.parametrize(
|
|
(
|
|
"action_variables",
|
|
"expected_variables",
|
|
"expected_attributes",
|
|
),
|
|
[
|
|
(
|
|
{"temperature": 22},
|
|
{"temperature": 22},
|
|
{"temperature": 22},
|
|
),
|
|
(
|
|
{"target_temp_low": 11, "target_temp_high": 29},
|
|
{"target_temp_low": 11, "target_temp_high": 29},
|
|
{"target_temp_low": 11, "target_temp_high": 29},
|
|
),
|
|
(
|
|
{"temperature": 20, "target_temp_low": 10, "target_temp_high": 30},
|
|
{"temperature": 20, "target_temp_low": 10, "target_temp_high": 30},
|
|
{"temperature": 20, "target_temp_low": 10, "target_temp_high": 30},
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.usefixtures("setup_climate")
|
|
async def test_optimistic_set_temperature_action_with_hvac_mode(
|
|
hass: HomeAssistant,
|
|
action_variables: ConfigType,
|
|
expected_variables: ConfigType,
|
|
expected_attributes: ConfigType,
|
|
calls: list[ServiceCall],
|
|
) -> None:
|
|
"""Test optimistic set_temperature action with hvac_mode."""
|
|
|
|
await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_ON)
|
|
|
|
assert_state_and_attributes(
|
|
hass,
|
|
TEST_CLIMATE,
|
|
STATE_UNKNOWN,
|
|
{"temperature": None, "target_temp_low": None, "target_temp_high": None},
|
|
)
|
|
|
|
await hass.services.async_call(
|
|
climate.DOMAIN,
|
|
"set_temperature",
|
|
{
|
|
"hvac_mode": "cool",
|
|
**action_variables,
|
|
ATTR_ENTITY_ID: TEST_CLIMATE.entity_id,
|
|
},
|
|
blocking=True,
|
|
)
|
|
assert_action(TEST_CLIMATE, calls, 2, "set_hvac_mode", 0, hvac_mode="cool")
|
|
assert_action(
|
|
TEST_CLIMATE,
|
|
calls,
|
|
2,
|
|
"set_temperature",
|
|
hvac_mode="cool",
|
|
**expected_variables,
|
|
)
|
|
|
|
assert_state_and_attributes(hass, TEST_CLIMATE, HVACMode.COOL, expected_attributes)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("config", "extra_config"), [(MINIMUM_REQUIREMENTS, SET_TEMPERATURE_ACTION)]
|
|
)
|
|
@pytest.mark.parametrize(
|
|
(
|
|
"action_variables",
|
|
"expected_variables",
|
|
"expected_attributes",
|
|
),
|
|
[
|
|
(
|
|
{"temperature": 21},
|
|
{"temperature": 21},
|
|
{"temperature": 21},
|
|
),
|
|
(
|
|
{"target_temp_low": 11, "target_temp_high": 29},
|
|
{"target_temp_low": 11, "target_temp_high": 29},
|
|
{"target_temp_low": 11, "target_temp_high": 29},
|
|
),
|
|
(
|
|
{"temperature": 20, "target_temp_low": 10, "target_temp_high": 30},
|
|
{"temperature": 20, "target_temp_low": 10, "target_temp_high": 30},
|
|
{"temperature": 20, "target_temp_low": 10, "target_temp_high": 30},
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.usefixtures("setup_climate")
|
|
async def test_optimistic_set_temperature_action_without_hvac_mode(
|
|
hass: HomeAssistant,
|
|
action_variables: ConfigType,
|
|
expected_variables: ConfigType,
|
|
expected_attributes: ConfigType,
|
|
calls: list[ServiceCall],
|
|
) -> None:
|
|
"""Test optimistic set_temperature action."""
|
|
|
|
await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_ON)
|
|
|
|
assert_state_and_attributes(
|
|
hass,
|
|
TEST_CLIMATE,
|
|
STATE_UNKNOWN,
|
|
{"temperature": None, "target_temp_low": None, "target_temp_high": None},
|
|
)
|
|
|
|
await _call_and_assert_action(
|
|
hass,
|
|
calls,
|
|
"set_temperature",
|
|
action_variables,
|
|
expected_variables,
|
|
"set_temperature",
|
|
)
|
|
assert_state_and_attributes(hass, TEST_CLIMATE, STATE_UNKNOWN, expected_attributes)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("attribute", "extra_config"),
|
|
[
|
|
(
|
|
"target_temperature_high",
|
|
{
|
|
"min_temperature": -3,
|
|
"max_temperature": 101,
|
|
"target_temperature_low": "{{ -3 }}",
|
|
**SET_TEMPERATURE_ACTION,
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
)
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("attribute_template", "expected"),
|
|
[
|
|
("{{ -1 }}", -1),
|
|
("{{ 5.3423 }}", 5.3),
|
|
("{{ 30 }}", 30),
|
|
("{{ 45 }}", 45),
|
|
("{{ 99 }}", 99),
|
|
("{{ 100 }}", 100),
|
|
("{{ 45.5 }}", 45.5),
|
|
("{{ -4 }}", None),
|
|
("{{ 103 }}", None),
|
|
("{{ True }}", None),
|
|
("{{ False }}", None),
|
|
("{{ 'something' }}", None),
|
|
("{{ x - 1 }}", None),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_single_attribute_climate")
|
|
async def test_target_temperature_high_template(
|
|
hass: HomeAssistant, expected: Any
|
|
) -> None:
|
|
"""Test template target_temperature_high."""
|
|
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state.attributes.get("target_temp_high") == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("attribute", "extra_config"),
|
|
[
|
|
(
|
|
"target_temperature_low",
|
|
{
|
|
"min_temperature": -2,
|
|
"max_temperature": 102,
|
|
"target_temperature_high": "{{ 102 }}",
|
|
**SET_TEMPERATURE_ACTION,
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
)
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("attribute_template", "expected"),
|
|
[
|
|
("{{ -1 }}", -1),
|
|
("{{ 5.3423 }}", 5.3),
|
|
("{{ 30 }}", 30),
|
|
("{{ 45 }}", 45),
|
|
("{{ 99 }}", 99),
|
|
("{{ 100 }}", 100),
|
|
("{{ 45.5 }}", 45.5),
|
|
("{{ -3 }}", None),
|
|
("{{ 103 }}", None),
|
|
("{{ True }}", None),
|
|
("{{ False }}", None),
|
|
("{{ 'something' }}", None),
|
|
("{{ x - 1 }}", None),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_single_attribute_climate")
|
|
async def test_target_temperature_low_template(
|
|
hass: HomeAssistant, expected: Any
|
|
) -> None:
|
|
"""Test template target_temperature_low."""
|
|
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state.attributes.get("target_temp_low") == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"config",
|
|
[
|
|
{"target_temperature_high": 30, **MINIMUM_REQUIREMENTS},
|
|
{"target_temperature_low": 30, **MINIMUM_REQUIREMENTS},
|
|
],
|
|
)
|
|
async def test_bad_target_temperature_range_config(
|
|
hass: HomeAssistant,
|
|
style: ConfigurationStyle,
|
|
config: ConfigType,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test a bad target temperature range configuration."""
|
|
platform = TEST_CLIMATE
|
|
await setup_entity(hass, platform, style, 0, config)
|
|
assert len(hass.states.async_all(platform.domain)) == 0
|
|
assert (
|
|
"Invalid config for 'template': some but not all values in the same group of inclusion 'temperature_limits'"
|
|
in caplog.text
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"config",
|
|
[
|
|
{
|
|
"target_temperature_high": 32,
|
|
"target_temperature_low": 17,
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
{"target_temperature": 21, **MINIMUM_REQUIREMENTS},
|
|
],
|
|
)
|
|
async def test_missing_set_temperature_config(
|
|
hass: HomeAssistant,
|
|
style: ConfigurationStyle,
|
|
config: ConfigType,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test a bad target temperature range configuration."""
|
|
platform = TEST_CLIMATE
|
|
await setup_entity(hass, platform, style, 0, config)
|
|
assert len(hass.states.async_all(platform.domain)) == 0
|
|
assert (
|
|
"Invalid config for 'template': Required option: 'set_temperature' is missing for option"
|
|
in caplog.text
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("attribute", "extra_config"),
|
|
[("hvac_modes", SET_HVAC_MODE_ACTION)],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("attribute_template", "expected"),
|
|
[
|
|
(
|
|
"{{ ['off', 'heat', 'cool', 'heat_cool'] }}",
|
|
[HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL, HVACMode.HEAT_COOL],
|
|
),
|
|
(
|
|
"{{ ['dry', 'auto', 'fan_only'] }}",
|
|
[HVACMode.DRY, HVACMode.AUTO, HVACMode.FAN_ONLY],
|
|
),
|
|
("{{ [] }}", []),
|
|
("{{ '[]' }}", []),
|
|
(
|
|
"{{ ['dry', 'auto2', 'fan_only'] }}",
|
|
[HVACMode.DRY, HVACMode.FAN_ONLY],
|
|
),
|
|
("{{ -3 }}", []),
|
|
("{{ 103.3 }}", []),
|
|
("{{ True }}", []),
|
|
("{{ False }}", []),
|
|
("{{ 'something' }}", []),
|
|
("{{ x - 1 }}", []),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_single_attribute_climate")
|
|
async def test_hvac_modes_template(
|
|
hass: HomeAssistant, attribute: str, expected: Any
|
|
) -> None:
|
|
"""Test hvac_modes template."""
|
|
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state.attributes.get(attribute) == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("attribute", "extra_config"),
|
|
[
|
|
(
|
|
"hvac_mode",
|
|
{
|
|
"hvac_modes": "{{ ['off', 'heat', 'cool', 'heat_cool', 'dry', 'auto'] }}",
|
|
**SET_HVAC_MODE_ACTION,
|
|
},
|
|
)
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("attribute_template", "expected"),
|
|
[
|
|
("{{ 'off' }}", HVACMode.OFF),
|
|
("{{ 'heat' }}", HVACMode.HEAT),
|
|
("{{ 'cool' }}", HVACMode.COOL),
|
|
("{{ 'heat_cool' }}", HVACMode.HEAT_COOL),
|
|
("{{ 'dry' }}", HVACMode.DRY),
|
|
("{{ 'auto' }}", HVACMode.AUTO),
|
|
("{{ 'fan_only' }}", STATE_UNKNOWN),
|
|
("{{ -3 }}", STATE_UNKNOWN),
|
|
("{{ 103.3 }}", STATE_UNKNOWN),
|
|
("{{ True }}", STATE_UNKNOWN),
|
|
("{{ False }}", STATE_UNKNOWN),
|
|
("{{ 'something' }}", STATE_UNKNOWN),
|
|
("{{ x - 1 }}", STATE_UNAVAILABLE),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_single_attribute_climate")
|
|
async def test_hvac_mode_template(hass: HomeAssistant, expected: Any) -> None:
|
|
"""Test hvac_mode template."""
|
|
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state.state == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("config", "option"),
|
|
[
|
|
(
|
|
SET_HVAC_MODE_ACTION,
|
|
"hvac_modes",
|
|
),
|
|
(
|
|
{
|
|
"hvac_modes": "{{ ['off', 'heat', 'cool', 'heat_cool', 'dry', 'auto', 'fan_only'] }}",
|
|
},
|
|
"set_hvac_mode",
|
|
),
|
|
],
|
|
)
|
|
async def test_required_hvac_mode_options(
|
|
hass: HomeAssistant,
|
|
style: ConfigurationStyle,
|
|
config: ConfigType,
|
|
option: str,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test missing required options."""
|
|
platform = TEST_CLIMATE
|
|
await setup_entity(hass, platform, style, 0, config)
|
|
assert len(hass.states.async_all(platform.domain)) == 0
|
|
assert (
|
|
f"Invalid config for 'template': required key '{option}' not provided"
|
|
in caplog.text
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("attribute", "attribute_template", "extra_config"),
|
|
[
|
|
(
|
|
"hvac_modes",
|
|
"{{ state_attr('sensor.test_attribute', 'hvac_modes') or [] }}",
|
|
SET_HVAC_MODE_ACTION,
|
|
)
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.usefixtures("setup_single_attribute_climate")
|
|
async def test_hvac_modes_updates_supported_features(hass: HomeAssistant) -> None:
|
|
"""Test hvac_modes updates supported features."""
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state.state == STATE_UNKNOWN
|
|
assert state.attributes["hvac_modes"] == []
|
|
assert state.attributes["supported_features"] == 0
|
|
|
|
await async_trigger(
|
|
hass,
|
|
TEST_ATTRIBUTE_ENTITY_ID,
|
|
"anything",
|
|
{"hvac_modes": ["heat"]},
|
|
)
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state.state == STATE_UNKNOWN
|
|
assert state.attributes["hvac_modes"] == [HVACMode.HEAT]
|
|
assert state.attributes["supported_features"] == ClimateEntityFeature.TURN_ON
|
|
|
|
await async_trigger(
|
|
hass,
|
|
TEST_ATTRIBUTE_ENTITY_ID,
|
|
"anything",
|
|
{"hvac_modes": ["off", "heat"]},
|
|
)
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state.state == STATE_UNKNOWN
|
|
assert state.attributes["hvac_modes"] == [HVACMode.OFF, HVACMode.HEAT]
|
|
assert (
|
|
state.attributes["supported_features"]
|
|
== ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
|
)
|
|
|
|
await async_trigger(
|
|
hass,
|
|
TEST_ATTRIBUTE_ENTITY_ID,
|
|
"anything",
|
|
{"hvac_modes": ["cool"]},
|
|
)
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state.state == STATE_UNKNOWN
|
|
assert state.attributes["hvac_modes"] == [HVACMode.COOL]
|
|
assert state.attributes["supported_features"] == ClimateEntityFeature.TURN_ON
|
|
|
|
|
|
@pytest.mark.parametrize(("config", "extra_config"), [(MINIMUM_REQUIREMENTS, {})])
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.usefixtures("setup_climate")
|
|
async def test_set_hvac_mode_action(
|
|
hass: HomeAssistant,
|
|
calls: list[ServiceCall],
|
|
) -> None:
|
|
"""Test setting valid group mode actions with template."""
|
|
|
|
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state is not None
|
|
|
|
await _call_and_assert_action(
|
|
hass,
|
|
calls,
|
|
"set_hvac_mode",
|
|
{"hvac_mode": "heat"},
|
|
{"hvac_mode": "heat"},
|
|
"set_hvac_mode",
|
|
)
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state is not None
|
|
|
|
with pytest.raises(ServiceValidationError):
|
|
await hass.services.async_call(
|
|
climate.DOMAIN,
|
|
"set_hvac_mode",
|
|
{"hvac_mode": "fan_only", ATTR_ENTITY_ID: TEST_CLIMATE.entity_id},
|
|
blocking=True,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(("config", "extra_config"), [(MINIMUM_REQUIREMENTS, {})])
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.usefixtures("setup_climate")
|
|
async def test_optimistic_set_hvac_mode_action(
|
|
hass: HomeAssistant,
|
|
calls: list[ServiceCall],
|
|
) -> None:
|
|
"""Test setting valid group mode actions with template."""
|
|
|
|
await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_ON)
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state is not None
|
|
|
|
await _call_and_assert_action(
|
|
hass,
|
|
calls,
|
|
"set_hvac_mode",
|
|
{"hvac_mode": "heat"},
|
|
{"hvac_mode": "heat"},
|
|
"set_hvac_mode",
|
|
)
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state is not None
|
|
assert state.state == HVACMode.HEAT
|
|
|
|
await _call_and_assert_action(
|
|
hass,
|
|
calls,
|
|
"set_hvac_mode",
|
|
{"hvac_mode": "cool"},
|
|
{"hvac_mode": "cool"},
|
|
"set_hvac_mode",
|
|
)
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state is not None
|
|
assert state.state == HVACMode.COOL
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("attribute", "extra_config"),
|
|
[
|
|
("fan_modes", {**SET_FAN_MODE_ACTION, **MINIMUM_REQUIREMENTS}),
|
|
("swing_modes", {**SET_SWING_MODE_ACTION, **MINIMUM_REQUIREMENTS}),
|
|
(
|
|
"swing_horizontal_modes",
|
|
{**SET_SWING_HORIZONTAL_MODE_ACTION, **MINIMUM_REQUIREMENTS},
|
|
),
|
|
(
|
|
"preset_modes",
|
|
{**SET_PRESET_MODE_ACTION, **MINIMUM_REQUIREMENTS},
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("attribute_template", "expected"),
|
|
[
|
|
("{{ ['off', 'low', 'medium', 'high'] }}", ["off", "low", "medium", "high"]),
|
|
("{{ ['off', 'high'] }}", ["off", "high"]),
|
|
("{{ [] }}", []),
|
|
("{{ '[]' }}", []),
|
|
("{{ -3 }}", None),
|
|
("{{ 103.3 }}", None),
|
|
("{{ True }}", None),
|
|
("{{ False }}", None),
|
|
("{{ 'something' }}", None),
|
|
("{{ x - 1 }}", None),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_single_attribute_climate")
|
|
async def test_group_modes_template(
|
|
hass: HomeAssistant, attribute: str, expected: Any
|
|
) -> None:
|
|
"""Test template modes for inclusive group."""
|
|
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state.attributes.get(attribute) == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("attribute", "extra_config"),
|
|
[
|
|
(
|
|
"fan_mode",
|
|
{
|
|
"fan_modes": "{{ ['off', 'low', 'medium', 'high'] }}",
|
|
**SET_FAN_MODE_ACTION,
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
),
|
|
(
|
|
"swing_mode",
|
|
{
|
|
"swing_modes": "{{ ['off', 'low', 'medium', 'high'] }}",
|
|
**SET_SWING_MODE_ACTION,
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
),
|
|
(
|
|
"swing_horizontal_mode",
|
|
{
|
|
"swing_horizontal_modes": "{{ ['off', 'low', 'medium', 'high'] }}",
|
|
**SET_SWING_HORIZONTAL_MODE_ACTION,
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
),
|
|
(
|
|
"preset_mode",
|
|
{
|
|
"preset_modes": "{{ ['off', 'low', 'medium', 'high'] }}",
|
|
**SET_PRESET_MODE_ACTION,
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("attribute_template", "expected"),
|
|
[
|
|
("{{ 'off' }}", "off"),
|
|
("{{ 'low' }}", "low"),
|
|
("{{ 'medium' }}", "medium"),
|
|
("{{ 'high' }}", "high"),
|
|
("{{ -3 }}", None),
|
|
("{{ 103.3 }}", None),
|
|
("{{ True }}", None),
|
|
("{{ False }}", None),
|
|
("{{ 'something' }}", None),
|
|
("{{ x - 1 }}", None),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_single_attribute_climate")
|
|
async def test_group_mode_template(
|
|
hass: HomeAssistant, attribute: str, expected: Any
|
|
) -> None:
|
|
"""Test template mode for inclusive group."""
|
|
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state.attributes.get(attribute) == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("config", "group"),
|
|
[
|
|
(
|
|
{**SET_FAN_MODE_ACTION, **MINIMUM_REQUIREMENTS},
|
|
"fan_mode",
|
|
),
|
|
(
|
|
{
|
|
"fan_modes": "{{ ['off', 'low', 'medium', 'high'] }}",
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
"fan_mode",
|
|
),
|
|
(
|
|
{**SET_SWING_MODE_ACTION, **MINIMUM_REQUIREMENTS},
|
|
"swing_mode",
|
|
),
|
|
(
|
|
{
|
|
"swing_modes": "{{ ['off', 'low', 'medium', 'high'] }}",
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
"swing_mode",
|
|
),
|
|
(
|
|
{**SET_SWING_HORIZONTAL_MODE_ACTION, **MINIMUM_REQUIREMENTS},
|
|
"swing_horizontal_mode",
|
|
),
|
|
(
|
|
{
|
|
"swing_horizontal_modes": "{{ ['off', 'low', 'medium', 'high'] }}",
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
"swing_horizontal_mode",
|
|
),
|
|
(
|
|
{**SET_PRESET_MODE_ACTION, **MINIMUM_REQUIREMENTS},
|
|
"preset_mode",
|
|
),
|
|
(
|
|
{
|
|
"preset_modes": "{{ ['off', 'low', 'medium', 'high'] }}",
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
"preset_mode",
|
|
),
|
|
],
|
|
)
|
|
async def test_bad_mode_group_config(
|
|
hass: HomeAssistant,
|
|
style: ConfigurationStyle,
|
|
config: ConfigType,
|
|
group: str,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test a bad mode group configuration."""
|
|
platform = TEST_CLIMATE
|
|
await setup_entity(hass, platform, style, 0, config)
|
|
assert len(hass.states.async_all(platform.domain)) == 0
|
|
assert (
|
|
f"Invalid config for 'template': Some required option(s) are missing from inclusive group '{group}', expected missing options"
|
|
in caplog.text
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("action", "attribute", "extra_config"),
|
|
[
|
|
(
|
|
"set_fan_mode",
|
|
"fan_mode",
|
|
{
|
|
"fan_modes": "{{ ['off', 'low', 'medium', 'high'] }}",
|
|
**SET_FAN_MODE_ACTION,
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
),
|
|
(
|
|
"set_swing_mode",
|
|
"swing_mode",
|
|
{
|
|
"swing_modes": "{{ ['off', 'low', 'medium', 'high'] }}",
|
|
**SET_SWING_MODE_ACTION,
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
),
|
|
(
|
|
"set_swing_horizontal_mode",
|
|
"swing_horizontal_mode",
|
|
{
|
|
"swing_horizontal_modes": "{{ ['off', 'low', 'medium', 'high'] }}",
|
|
**SET_SWING_HORIZONTAL_MODE_ACTION,
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
),
|
|
(
|
|
"set_preset_mode",
|
|
"preset_mode",
|
|
{
|
|
"preset_modes": "{{ ['off', 'low', 'medium', 'high'] }}",
|
|
**SET_PRESET_MODE_ACTION,
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("attribute_template", "mode", "expected"),
|
|
[
|
|
("{{ 'off' }}", "off", "off"),
|
|
("{{ 'low' }}", "low", "low"),
|
|
("{{ 'medium' }}", "medium", "medium"),
|
|
("{{ 'high' }}", "high", "high"),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_single_attribute_climate")
|
|
async def test_set_group_actions(
|
|
hass: HomeAssistant,
|
|
action: str,
|
|
attribute: str,
|
|
mode: str,
|
|
expected: Any,
|
|
calls: list[ServiceCall],
|
|
) -> None:
|
|
"""Test setting valid group mode actions with template."""
|
|
|
|
await async_trigger(hass, TEST_STATE_ENTITY_ID, STATE_ON)
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state is not None
|
|
|
|
await _call_and_assert_action(
|
|
hass, calls, action, {attribute: mode}, {attribute: mode}, action
|
|
)
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state is not None
|
|
assert state.attributes.get(attribute) == expected
|
|
|
|
with pytest.raises(ServiceValidationError):
|
|
await hass.services.async_call(
|
|
climate.DOMAIN,
|
|
action,
|
|
{attribute: "turbo", ATTR_ENTITY_ID: TEST_CLIMATE.entity_id},
|
|
blocking=True,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("action", "attribute", "config"),
|
|
[
|
|
(
|
|
"set_fan_mode",
|
|
"fan_mode",
|
|
{
|
|
"fan_modes": "{{ ['off', 'low', 'medium', 'high'] }}",
|
|
**SET_FAN_MODE_ACTION,
|
|
},
|
|
),
|
|
(
|
|
"set_swing_mode",
|
|
"swing_mode",
|
|
{
|
|
"swing_modes": "{{ ['off', 'low', 'medium', 'high'] }}",
|
|
**SET_SWING_MODE_ACTION,
|
|
},
|
|
),
|
|
(
|
|
"set_swing_horizontal_mode",
|
|
"swing_horizontal_mode",
|
|
{
|
|
"swing_horizontal_modes": "{{ ['off', 'low', 'medium', 'high'] }}",
|
|
**SET_SWING_HORIZONTAL_MODE_ACTION,
|
|
},
|
|
),
|
|
(
|
|
"set_preset_mode",
|
|
"preset_mode",
|
|
{
|
|
"preset_modes": "{{ ['off', 'low', 'medium', 'high'] }}",
|
|
**SET_PRESET_MODE_ACTION,
|
|
},
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"extra_config",
|
|
[MINIMUM_REQUIREMENTS],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("mode", "expected"),
|
|
[
|
|
("off", "off"),
|
|
("low", "low"),
|
|
("medium", "medium"),
|
|
("high", "high"),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_climate")
|
|
async def test_group_optimistic_actions(
|
|
hass: HomeAssistant,
|
|
action: str,
|
|
attribute: str,
|
|
mode: str,
|
|
expected: Any,
|
|
calls: list[ServiceCall],
|
|
) -> None:
|
|
"""Test setting valid group mode actions with template."""
|
|
|
|
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state is not None
|
|
assert state.state == STATE_UNKNOWN
|
|
|
|
await _call_and_assert_action(
|
|
hass, calls, action, {attribute: mode}, {attribute: mode}, action
|
|
)
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state is not None
|
|
assert state.attributes.get(attribute) == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("option", "option_type"),
|
|
[
|
|
("max_humidity", "int"),
|
|
("min_humidity", "int"),
|
|
("max_temperature", "float"),
|
|
("min_temperature", "float"),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize("value", ["not a number", None])
|
|
async def test_bad_min_max_options(
|
|
hass: HomeAssistant,
|
|
style: ConfigurationStyle,
|
|
option: str,
|
|
option_type: str,
|
|
value: Any,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test a bad min max options in configuration."""
|
|
platform = TEST_CLIMATE
|
|
await setup_entity(
|
|
hass, platform, style, 0, {option: value, **MINIMUM_REQUIREMENTS}
|
|
)
|
|
assert len(hass.states.async_all(platform.domain)) == 0
|
|
assert (
|
|
f"Invalid config for 'template': expected {option_type} for dictionary value 'climate->0->{option}'"
|
|
in caplog.text
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"config",
|
|
[
|
|
{
|
|
"target_temperature": "{{ state_attr('sensor.test_attribute', 'value') or 0.0 }}",
|
|
**SET_TEMPERATURE_ACTION,
|
|
**MINIMUM_REQUIREMENTS,
|
|
}
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("extra_config", "set_attribute", "expected"),
|
|
[
|
|
({"target_temperature_step": 0.1}, 7.54, 7.5),
|
|
({"target_temperature_step": 1.0}, 7.54, 8),
|
|
({"target_temperature_step": 5.0}, 7.54, 10),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_climate")
|
|
async def test_target_temperature_step(
|
|
hass: HomeAssistant, set_attribute: float, expected: float
|
|
) -> None:
|
|
"""Test target temperature step."""
|
|
await async_trigger(
|
|
hass, TEST_ATTRIBUTE_ENTITY_ID, "anything", {"value": set_attribute}
|
|
)
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state.attributes.get("temperature") == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"config",
|
|
[
|
|
{
|
|
"target_humidity": "{{ state_attr('sensor.test_attribute', 'value') or 0.0 }}",
|
|
**SET_HUMIDITY_ACTION,
|
|
**MINIMUM_REQUIREMENTS,
|
|
}
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("extra_config", "set_attribute", "expected"),
|
|
[
|
|
({"target_humidity_step": 1}, 44, 44),
|
|
({"target_humidity_step": 5}, 44, 45),
|
|
({"target_humidity_step": 7}, 44, 42),
|
|
({"target_humidity_step": 10}, 44, 40),
|
|
({"target_humidity_step": 15}, 44, 45),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_climate")
|
|
async def test_target_humidity_step(
|
|
hass: HomeAssistant, set_attribute: int, expected: int
|
|
) -> None:
|
|
"""Test target humidity step."""
|
|
await async_trigger(
|
|
hass, TEST_ATTRIBUTE_ENTITY_ID, "anything", {"value": set_attribute}
|
|
)
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state.attributes.get("humidity") == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("option", "option_type", "minimum"),
|
|
[
|
|
("target_humidity_step", "int", 1),
|
|
("target_temperature_step", "float", 0.1),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize("value", [-1, 0, "not a number", None])
|
|
async def test_bad_step_options(
|
|
hass: HomeAssistant,
|
|
style: ConfigurationStyle,
|
|
option: str,
|
|
option_type: str,
|
|
value: Any,
|
|
minimum: float,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test a bad step options in configuration."""
|
|
platform = TEST_CLIMATE
|
|
await setup_entity(
|
|
hass, platform, style, 0, {option: value, **MINIMUM_REQUIREMENTS}
|
|
)
|
|
assert len(hass.states.async_all(platform.domain)) == 0
|
|
assert (
|
|
f"Invalid config for 'template': expected {option_type} for dictionary value 'climate->0->{option}'"
|
|
in caplog.text
|
|
) or (
|
|
f"Invalid config for 'template': value must be at least {minimum} for dictionary value 'climate->0->{option}'"
|
|
in caplog.text
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("value", "expected"),
|
|
[(0.5, 1.5), (0.1, 1.4), (1, 1)],
|
|
)
|
|
async def test_precision_option(
|
|
hass: HomeAssistant, style: ConfigurationStyle, value: float, expected: float
|
|
) -> None:
|
|
"""Test precision option."""
|
|
platform = TEST_CLIMATE
|
|
await setup_entity(
|
|
hass,
|
|
platform,
|
|
style,
|
|
1,
|
|
{
|
|
"precision": value,
|
|
"current_temperature": "{{ 1.4 }}",
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
)
|
|
|
|
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
|
|
|
|
state = hass.states.get(TEST_CLIMATE.entity_id)
|
|
assert state.attributes.get("current_temperature") == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
@pytest.mark.parametrize("value", [-1, 0.0, "not a number", False, None])
|
|
async def test_bad_precision_option(
|
|
hass: HomeAssistant,
|
|
style: ConfigurationStyle,
|
|
value: Any,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test a bad precision option."""
|
|
platform = TEST_CLIMATE
|
|
await setup_entity(
|
|
hass, platform, style, 0, {"precision": value, **MINIMUM_REQUIREMENTS}
|
|
)
|
|
|
|
assert len(hass.states.async_all(platform.domain)) == 0
|
|
assert (
|
|
"Invalid config for 'template': expected 0.5 or 0.1 or 1 for dictionary value 'climate->0->precision'"
|
|
in caplog.text
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"value",
|
|
[UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS, UnitOfTemperature.KELVIN],
|
|
)
|
|
async def test_temperature_unit(
|
|
hass: HomeAssistant, style: ConfigurationStyle, value: str
|
|
) -> None:
|
|
"""Test temperature_unit option."""
|
|
platform = TEST_CLIMATE
|
|
await setup_entity(
|
|
hass,
|
|
platform,
|
|
style,
|
|
1,
|
|
{
|
|
"temperature_unit": value,
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
)
|
|
|
|
assert len(hass.states.async_all(platform.domain)) == 1
|
|
assert hass.states.get(TEST_CLIMATE.entity_id)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
@pytest.mark.parametrize("value", [-1, 0.0, "not a number", False, None])
|
|
async def test_bad_temperature_unit(
|
|
hass: HomeAssistant,
|
|
style: ConfigurationStyle,
|
|
value: Any,
|
|
) -> None:
|
|
"""Test a bad temperature_unit option."""
|
|
platform = TEST_CLIMATE
|
|
await setup_entity(
|
|
hass, platform, style, 0, {"temperature_unit": value, **MINIMUM_REQUIREMENTS}
|
|
)
|
|
|
|
assert len(hass.states.async_all(platform.domain)) == 0
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("extra_config", "attribute_template"),
|
|
[
|
|
(
|
|
{"hvac_mode": "{{ states('sensor.test_state') }}", **MINIMUM_REQUIREMENTS},
|
|
"{{ is_state('binary_sensor.availability', 'on') }}",
|
|
)
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("style", "attribute"),
|
|
[
|
|
(ConfigurationStyle.MODERN, "availability"),
|
|
(ConfigurationStyle.TRIGGER, "availability"),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_single_attribute_climate")
|
|
async def test_available_template_with_entities(hass: HomeAssistant) -> None:
|
|
"""Test availability templates with values from other entities."""
|
|
hass.states.async_set(TEST_AVAILABILITY_ENTITY, STATE_ON)
|
|
await hass.async_block_till_done()
|
|
|
|
await async_trigger(hass, TEST_STATE_ENTITY_ID, HVACMode.HEAT)
|
|
|
|
assert hass.states.get(TEST_CLIMATE.entity_id).state != STATE_UNAVAILABLE
|
|
|
|
hass.states.async_set(TEST_AVAILABILITY_ENTITY, STATE_OFF)
|
|
await hass.async_block_till_done()
|
|
|
|
await async_trigger(hass, TEST_STATE_ENTITY_ID, HVACMode.COOL)
|
|
|
|
assert hass.states.get(TEST_CLIMATE.entity_id).state == STATE_UNAVAILABLE
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("extra_config", "attribute_template"),
|
|
[
|
|
(
|
|
{"hvac_mode": "{{ states('sensor.test_state') }}", **MINIMUM_REQUIREMENTS},
|
|
"{{ x - 12 }}",
|
|
)
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("style", "attribute"),
|
|
[
|
|
(ConfigurationStyle.MODERN, "availability"),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("setup_single_attribute_climate")
|
|
async def test_invalid_availability_template_keeps_component_available(
|
|
hass: HomeAssistant, caplog_setup_text: str
|
|
) -> None:
|
|
"""Test that an invalid availability keeps the device available."""
|
|
assert hass.states.get(TEST_CLIMATE.entity_id).state != STATE_UNAVAILABLE
|
|
assert "UndefinedError: 'x' is undefined" in caplog_setup_text
|
|
|
|
|
|
@pytest.mark.parametrize("config", [MINIMUM_REQUIREMENTS])
|
|
@pytest.mark.parametrize(
|
|
"style",
|
|
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
|
)
|
|
async def test_unique_id(
|
|
hass: HomeAssistant, style: ConfigurationStyle, config: ConfigType
|
|
) -> None:
|
|
"""Test unique_id option only creates one entity per id."""
|
|
await setup_and_test_unique_id(hass, TEST_CLIMATE, style, config)
|
|
|
|
|
|
@pytest.mark.parametrize("config", [MINIMUM_REQUIREMENTS])
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
async def test_nested_unique_id(
|
|
hass: HomeAssistant,
|
|
style: ConfigurationStyle,
|
|
config: ConfigType,
|
|
entity_registry: er.EntityRegistry,
|
|
) -> None:
|
|
"""Test a template unique_id propagates to entity unique_ids."""
|
|
await setup_and_test_nested_unique_id(
|
|
hass, TEST_CLIMATE, style, entity_registry, config
|
|
)
|
|
|
|
|
|
async def test_setup_config_entry(
|
|
hass: HomeAssistant,
|
|
snapshot: SnapshotAssertion,
|
|
) -> None:
|
|
"""Tests creating a entity from a config entry."""
|
|
|
|
template_config_entry = MockConfigEntry(
|
|
data={},
|
|
domain=template.DOMAIN,
|
|
options={
|
|
"name": "My template",
|
|
"hvac_mode": "{{ 'heat' }}",
|
|
**MINIMUM_REQUIREMENTS,
|
|
"template_type": climate.DOMAIN,
|
|
},
|
|
title="My template",
|
|
)
|
|
template_config_entry.add_to_hass(hass)
|
|
|
|
assert await hass.config_entries.async_setup(template_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get("climate.my_template")
|
|
assert state is not None
|
|
assert state == snapshot
|
|
|
|
|
|
async def test_flow_preview(
|
|
hass: HomeAssistant,
|
|
hass_ws_client: WebSocketGenerator,
|
|
) -> None:
|
|
"""Test the config flow preview."""
|
|
|
|
state = await async_get_flow_preview_state(
|
|
hass,
|
|
hass_ws_client,
|
|
climate.DOMAIN,
|
|
{"name": "My template", "hvac_mode": "{{ 'heat' }}", **MINIMUM_REQUIREMENTS},
|
|
)
|
|
|
|
assert state["state"] == HVACMode.HEAT
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
@pytest.mark.parametrize(
|
|
(
|
|
"saved_state",
|
|
"saved_extra_data",
|
|
"initial_state",
|
|
"initial_attributes",
|
|
),
|
|
[
|
|
(
|
|
HVACMode.COOL,
|
|
{
|
|
"current_humidity": None,
|
|
"current_temperature": 35.0,
|
|
"fan_mode": None,
|
|
"fan_modes": None,
|
|
"hvac_action": "idle",
|
|
"hvac_mode": "cool",
|
|
"hvac_modes": ["cool"],
|
|
"preset_mode": None,
|
|
"preset_modes": None,
|
|
"swing_mode": None,
|
|
"swing_modes": None,
|
|
"swing_horizontal_mode": None,
|
|
"swing_horizontal_modes": None,
|
|
"target_humidity": None,
|
|
"target_temperature_high": None,
|
|
"target_temperature_low": None,
|
|
"target_temperature": None,
|
|
},
|
|
HVACMode.COOL,
|
|
{
|
|
"current_humidity": None,
|
|
"current_temperature": 35.0,
|
|
"fan_mode": None,
|
|
"fan_modes": None,
|
|
"hvac_action": HVACAction.IDLE,
|
|
"hvac_modes": [HVACMode.COOL],
|
|
"preset_mode": None,
|
|
"preset_modes": None,
|
|
"swing_mode": None,
|
|
"swing_modes": None,
|
|
"swing_horizontal_mode": None,
|
|
"swing_horizontal_modes": None,
|
|
"humidity": None,
|
|
"target_temp_high": None,
|
|
"target_temp_low": None,
|
|
"temperature": None,
|
|
},
|
|
),
|
|
(
|
|
# Missing key
|
|
HVACMode.COOL,
|
|
{
|
|
"current_humidity": None,
|
|
"current_temperature": 35.0,
|
|
"fan_mode": None,
|
|
"fan_modes": None,
|
|
"hvac_action": "idle",
|
|
"hvac_mode": "cool",
|
|
"hvac_modes": ["cool"],
|
|
"preset_mode": None,
|
|
"preset_modes": None,
|
|
"swing_mode": None,
|
|
"swing_modes": None,
|
|
"swing_horizontal_mode": None,
|
|
"target_humidity": None,
|
|
"target_temperature_high": None,
|
|
"target_temperature_low": None,
|
|
"target_temperature": None,
|
|
},
|
|
STATE_UNKNOWN,
|
|
{},
|
|
),
|
|
(
|
|
# Bad hvac mode
|
|
HVACMode.COOL,
|
|
{
|
|
"current_humidity": None,
|
|
"current_temperature": 35.0,
|
|
"fan_mode": None,
|
|
"fan_modes": None,
|
|
"hvac_action": "idle",
|
|
"hvac_mode": "not_cool",
|
|
"hvac_modes": ["cool"],
|
|
"preset_mode": None,
|
|
"preset_modes": None,
|
|
"swing_mode": None,
|
|
"swing_modes": None,
|
|
"swing_horizontal_mode": None,
|
|
"swing_horizontal_modes": None,
|
|
"target_humidity": None,
|
|
"target_temperature_high": None,
|
|
"target_temperature_low": None,
|
|
"target_temperature": None,
|
|
},
|
|
STATE_UNKNOWN,
|
|
{},
|
|
),
|
|
(
|
|
# Bad supported hvac modes
|
|
HVACMode.COOL,
|
|
{
|
|
"current_humidity": None,
|
|
"current_temperature": 35.0,
|
|
"fan_mode": None,
|
|
"fan_modes": None,
|
|
"hvac_action": "idle",
|
|
"hvac_mode": "cool",
|
|
"hvac_modes": ["not_cool"],
|
|
"preset_mode": None,
|
|
"preset_modes": None,
|
|
"swing_mode": None,
|
|
"swing_modes": None,
|
|
"swing_horizontal_mode": None,
|
|
"swing_horizontal_modes": None,
|
|
"target_humidity": None,
|
|
"target_temperature_high": None,
|
|
"target_temperature_low": None,
|
|
"target_temperature": None,
|
|
},
|
|
STATE_UNKNOWN,
|
|
{},
|
|
),
|
|
(
|
|
# Bad hvac action
|
|
HVACMode.COOL,
|
|
{
|
|
"current_humidity": None,
|
|
"current_temperature": 35.0,
|
|
"fan_mode": None,
|
|
"fan_modes": None,
|
|
"hvac_action": "idlex",
|
|
"hvac_mode": "cool",
|
|
"hvac_modes": ["cool"],
|
|
"preset_mode": None,
|
|
"preset_modes": None,
|
|
"swing_mode": None,
|
|
"swing_modes": None,
|
|
"swing_horizontal_mode": None,
|
|
"swing_horizontal_modes": None,
|
|
"target_humidity": None,
|
|
"target_temperature_high": None,
|
|
"target_temperature_low": None,
|
|
"target_temperature": None,
|
|
},
|
|
STATE_UNKNOWN,
|
|
{},
|
|
),
|
|
(
|
|
STATE_UNAVAILABLE,
|
|
{
|
|
"current_humidity": None,
|
|
"current_temperature": 35.0,
|
|
"fan_mode": None,
|
|
"fan_modes": None,
|
|
"hvac_action": "idle",
|
|
"hvac_mode": "cool",
|
|
"hvac_modes": ["cool"],
|
|
"preset_mode": None,
|
|
"preset_modes": None,
|
|
"swing_mode": None,
|
|
"swing_modes": None,
|
|
"swing_horizontal_mode": None,
|
|
"swing_horizontal_modes": None,
|
|
"target_humidity": None,
|
|
"target_temperature_high": None,
|
|
"target_temperature_low": None,
|
|
"target_temperature": None,
|
|
},
|
|
STATE_UNKNOWN,
|
|
{},
|
|
),
|
|
(
|
|
STATE_UNKNOWN,
|
|
{
|
|
"current_humidity": None,
|
|
"current_temperature": 35.0,
|
|
"fan_mode": None,
|
|
"fan_modes": None,
|
|
"hvac_action": "idle",
|
|
"hvac_mode": None,
|
|
"hvac_modes": ["cool"],
|
|
"preset_mode": None,
|
|
"preset_modes": None,
|
|
"swing_mode": None,
|
|
"swing_modes": None,
|
|
"swing_horizontal_mode": None,
|
|
"swing_horizontal_modes": None,
|
|
"target_humidity": None,
|
|
"target_temperature_high": None,
|
|
"target_temperature_low": None,
|
|
"target_temperature": None,
|
|
},
|
|
STATE_UNKNOWN,
|
|
{},
|
|
),
|
|
],
|
|
)
|
|
async def test_restore_state(
|
|
hass: HomeAssistant,
|
|
style: ConfigurationStyle,
|
|
saved_state: str,
|
|
saved_extra_data: dict | None,
|
|
initial_state: str,
|
|
initial_attributes: ConfigType,
|
|
) -> None:
|
|
"""Test restoring trigger template climate."""
|
|
|
|
restored_attributes = { # These should be ignored
|
|
"current_position": 5,
|
|
"current_tilt_position": 5,
|
|
}
|
|
|
|
setup_mock_template_entity_restore_state(
|
|
hass,
|
|
TEST_CLIMATE,
|
|
saved_state,
|
|
saved_extra_data=saved_extra_data,
|
|
saved_attributes=restored_attributes,
|
|
)
|
|
|
|
await setup_restore_template_entity(
|
|
hass,
|
|
TEST_CLIMATE,
|
|
style,
|
|
{
|
|
"current_humidity": "{{ state_attr('sensor.test_state', 'current_humidity') }}",
|
|
"current_temperature": "{{ state_attr('sensor.test_state', 'current_temperature') }}",
|
|
"fan_mode": "{{ state_attr('sensor.test_state', 'fan_mode') }}",
|
|
"fan_modes": "{{ state_attr('sensor.test_state', 'fan_modes') or [] }}",
|
|
"set_fan_mode": [],
|
|
"hvac_mode": "{{ state_attr('sensor.test_state', 'hvac_mode') }}",
|
|
"hvac_modes": "{{ state_attr('sensor.test_state', 'hvac_modes') or [] }}",
|
|
"set_hvac_mode": [],
|
|
"preset_mode": "{{ state_attr('sensor.test_state', 'preset_mode') }}",
|
|
"preset_modes": "{{ state_attr('sensor.test_state', 'preset_modes') or [] }}",
|
|
"set_preset_mode": [],
|
|
"swing_horizontal_mode": "{{ state_attr('sensor.test_state', 'swing_horizontal_mode') }}",
|
|
"swing_horizontal_modes": "{{ state_attr('sensor.test_state', 'swing_horizontal_modes') or [] }}",
|
|
"set_swing_horizontal_mode": [],
|
|
"swing_mode": "{{ state_attr('sensor.test_state', 'swing_mode') }}",
|
|
"swing_modes": "{{ state_attr('sensor.test_state', 'swing_modes') or [] }}",
|
|
"set_swing_mode": [],
|
|
"target_humidity": "{{ state_attr('sensor.test_state', 'target_humidity') }}",
|
|
"target_temperature": "{{ state_attr('sensor.test_state', 'target_temperature') }}",
|
|
"target_temperature_high": "{{ state_attr('sensor.test_state', 'target_temperature_high') }}",
|
|
"target_temperature_low": "{{ state_attr('sensor.test_state', 'target_temperature_low') }}",
|
|
"set_temperature": [],
|
|
"set_humidity": [],
|
|
},
|
|
"is_state_attr('sensor.test_state', 'hvac_mode', 'heat')",
|
|
)
|
|
|
|
assert_state_and_attributes(
|
|
hass,
|
|
TEST_CLIMATE,
|
|
initial_state,
|
|
initial_attributes,
|
|
)
|
|
|
|
await async_trigger(hass, "sensor.test_state", "x", {"hvac_modes": ["heat"]})
|
|
await async_trigger(
|
|
hass,
|
|
"sensor.test_state",
|
|
"x",
|
|
{"hvac_modes": ["heat"], "hvac_mode": HVACMode.HEAT},
|
|
)
|
|
|
|
assert_state_and_attributes(hass, TEST_CLIMATE, HVACMode.HEAT)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
async def test_saving_state(
|
|
hass: HomeAssistant,
|
|
style: ConfigurationStyle,
|
|
hass_storage: dict[str, Any],
|
|
) -> None:
|
|
"""Test restore saved state."""
|
|
|
|
await setup_entity(
|
|
hass,
|
|
TEST_CLIMATE,
|
|
style,
|
|
1,
|
|
config={
|
|
"hvac_mode": "{{ state_attr('sensor.test_state', 'hvac_mode') }}",
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
)
|
|
|
|
await async_trigger(
|
|
hass,
|
|
TEST_STATE_ENTITY_ID,
|
|
"anything",
|
|
{"hvac_mode": HVACMode.COOL},
|
|
)
|
|
|
|
assert_state_and_attributes(
|
|
hass, TEST_CLIMATE, HVACMode.COOL, {"hvac_modes": EXPECTED_HVAC_MODES}
|
|
)
|
|
|
|
await async_mock_restore_state_shutdown_restart(hass)
|
|
|
|
assert len(hass_storage[RESTORE_STATE_KEY]["data"]) == 1
|
|
state = hass_storage[RESTORE_STATE_KEY]["data"][0]["state"]
|
|
assert state["entity_id"] == TEST_CLIMATE.entity_id
|
|
|
|
extra_data = hass_storage[RESTORE_STATE_KEY]["data"][0]["extra_data"]
|
|
assert extra_data == {
|
|
"current_humidity": None,
|
|
"current_temperature": None,
|
|
"fan_mode": None,
|
|
"fan_modes": None,
|
|
"hvac_action": None,
|
|
"hvac_mode": "cool",
|
|
"hvac_modes": EXPECTED_HVAC_MODES,
|
|
"preset_mode": None,
|
|
"preset_modes": None,
|
|
"swing_mode": None,
|
|
"swing_modes": None,
|
|
"swing_horizontal_mode": None,
|
|
"swing_horizontal_modes": None,
|
|
"target_humidity": None,
|
|
"target_temperature_high": None,
|
|
"target_temperature_low": None,
|
|
"target_temperature": None,
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("action", "config"),
|
|
[
|
|
(
|
|
"set_fan_mode",
|
|
{
|
|
"fan_modes": "{{ ['Disco', 'Police'] }}",
|
|
"fan_mode": "{{ None }}",
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
),
|
|
("set_humidity", MINIMUM_REQUIREMENTS),
|
|
("set_hvac_mode", HVAC_MODES),
|
|
(
|
|
"set_preset_mode",
|
|
{
|
|
"preset_modes": "{{ ['Disco', 'Police'] }}",
|
|
"preset_mode": "{{ None }}",
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
),
|
|
(
|
|
"set_swing_horizontal_mode",
|
|
{
|
|
"swing_horizontal_modes": "{{ ['Disco', 'Police'] }}",
|
|
"swing_horizontal_mode": "{{ None }}",
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
),
|
|
(
|
|
"set_swing_mode",
|
|
{
|
|
"swing_modes": "{{ ['Disco', 'Police'] }}",
|
|
"swing_mode": "{{ None }}",
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
),
|
|
("set_temperature", MINIMUM_REQUIREMENTS),
|
|
],
|
|
)
|
|
async def test_invalid_yaml_actions_do_not_create_entities(
|
|
hass: HomeAssistant,
|
|
style: ConfigurationStyle,
|
|
action: str,
|
|
config: ConfigType,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test invalid yaml actions do not create entities."""
|
|
await assert_invalid_yaml_actions_do_not_create_entities(
|
|
hass, TEST_CLIMATE, style, config, action, caplog
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("action", "config"),
|
|
[
|
|
(
|
|
"set_fan_mode",
|
|
{
|
|
"fan_modes": "{{ ['Disco', 'Police'] }}",
|
|
"fan_mode": "{{ None }}",
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
),
|
|
("set_humidity", MINIMUM_REQUIREMENTS),
|
|
("set_hvac_mode", HVAC_MODES),
|
|
(
|
|
"set_preset_mode",
|
|
{
|
|
"preset_modes": "{{ ['Disco', 'Police'] }}",
|
|
"preset_mode": "{{ None }}",
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
),
|
|
(
|
|
"set_swing_horizontal_mode",
|
|
{
|
|
"swing_horizontal_modes": "{{ ['Disco', 'Police'] }}",
|
|
"swing_horizontal_mode": "{{ None }}",
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
),
|
|
(
|
|
"set_swing_mode",
|
|
{
|
|
"swing_modes": "{{ ['Disco', 'Police'] }}",
|
|
"swing_mode": "{{ None }}",
|
|
**MINIMUM_REQUIREMENTS,
|
|
},
|
|
),
|
|
("set_temperature", MINIMUM_REQUIREMENTS),
|
|
],
|
|
)
|
|
async def test_invalid_config_entry_actions_do_not_create_entities(
|
|
hass: HomeAssistant,
|
|
action: str,
|
|
config: ConfigType,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test invalid config entry actions do not create entities."""
|
|
await assert_invalid_config_entry_actions_do_not_create_entities(
|
|
hass, TEST_CLIMATE, config, action, caplog
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
async def test_extra_template_attributes(
|
|
hass: HomeAssistant, style: ConfigurationStyle
|
|
) -> None:
|
|
"""Test extra attributes."""
|
|
await assert_extra_template_attributes(
|
|
hass, TEST_CLIMATE, style, MINIMUM_REQUIREMENTS
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"attribute",
|
|
list(chain(ClimateEntityCapabilityAttribute, ClimateEntityStateAttribute)),
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
async def test_blocked_template_attributes(
|
|
hass: HomeAssistant,
|
|
style: ConfigurationStyle,
|
|
attribute: StrEnum,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test blocked extra attributes."""
|
|
await setup_entity(
|
|
hass,
|
|
TEST_CLIMATE,
|
|
style,
|
|
0,
|
|
{
|
|
**MINIMUM_REQUIREMENTS,
|
|
"attributes": {str(attribute): "{{ 'does not matter' }}"},
|
|
},
|
|
)
|
|
assert (
|
|
f"Unsupported attribute(s) found for {DEFAULT_NAME}: {attribute}" in caplog.text
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
async def test_attributes_template(
|
|
hass: HomeAssistant,
|
|
style: ConfigurationStyle,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test attributes as a single template."""
|
|
await assert_attributes_template(
|
|
hass,
|
|
TEST_CLIMATE,
|
|
style,
|
|
MINIMUM_REQUIREMENTS,
|
|
caplog,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"attribute",
|
|
list(chain(ClimateEntityCapabilityAttribute, ClimateEntityStateAttribute)),
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
|
|
)
|
|
async def test_attributes_template_with_blocked_attributes(
|
|
hass: HomeAssistant,
|
|
style: ConfigurationStyle,
|
|
attribute: StrEnum,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test blocked attributes for a single attributes template."""
|
|
await setup_entity(
|
|
hass,
|
|
TEST_CLIMATE,
|
|
style,
|
|
1,
|
|
{
|
|
**MINIMUM_REQUIREMENTS,
|
|
"attributes": f"{{{{ dict({attribute}='does not matter') }}}}",
|
|
},
|
|
)
|
|
|
|
await async_trigger(hass, "sensor.test_extra_attributes", "anything")
|
|
|
|
error = f"Unsupported attribute(s) found for {TEST_CLIMATE.entity_id}: {attribute}"
|
|
assert error in caplog.text
|