"""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