From 4a1905a2a2a6e7b766776e4fc3b6efbf5dbfefb4 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Fri, 25 Apr 2025 09:22:49 -0400 Subject: [PATCH] Update template cover to modern style config (#141878) --- homeassistant/components/template/config.py | 7 +- homeassistant/components/template/cover.py | 123 +- tests/components/template/test_cover.py | 1808 ++++++++++--------- 3 files changed, 1075 insertions(+), 863 deletions(-) diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index 4e07d67f6e9..ca8579f7734 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -12,6 +12,7 @@ from homeassistant.components.blueprint import ( is_blueprint_instance_config, ) from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN @@ -37,6 +38,7 @@ from homeassistant.setup import async_notify_setup_error from . import ( binary_sensor as binary_sensor_platform, button as button_platform, + cover as cover_platform, image as image_platform, light as light_platform, number as number_platform, @@ -117,9 +119,12 @@ CONFIG_SECTION_SCHEMA = vol.Schema( vol.Optional(SWITCH_DOMAIN): vol.All( cv.ensure_list, [switch_platform.SWITCH_SCHEMA] ), + vol.Optional(COVER_DOMAIN): vol.All( + cv.ensure_list, [cover_platform.COVER_SCHEMA] + ), }, ensure_domains_do_not_have_trigger_or_action( - BUTTON_DOMAIN, LIGHT_DOMAIN, SWITCH_DOMAIN + BUTTON_DOMAIN, COVER_DOMAIN, LIGHT_DOMAIN, SWITCH_DOMAIN ), ) ) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 7c9c0ea9d53..e15180173b4 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -21,20 +21,25 @@ from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FRIENDLY_NAME, + CONF_NAME, CONF_OPTIMISTIC, + CONF_STATE, CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import DOMAIN +from .const import CONF_OBJECT_ID, CONF_PICTURE, DOMAIN from .template_entity import ( + LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS, + TEMPLATE_ENTITY_AVAILABILITY_SCHEMA, TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY, + TEMPLATE_ENTITY_ICON_SCHEMA, TemplateEntity, rewrite_common_legacy_to_modern_conf, ) @@ -56,7 +61,9 @@ _VALID_STATES = [ "none", ] +CONF_POSITION = "position" CONF_POSITION_TEMPLATE = "position_template" +CONF_TILT = "tilt" CONF_TILT_TEMPLATE = "tilt_template" OPEN_ACTION = "open_cover" CLOSE_ACTION = "close_cover" @@ -74,7 +81,39 @@ TILT_FEATURES = ( | CoverEntityFeature.SET_TILT_POSITION ) +LEGACY_FIELDS = TEMPLATE_ENTITY_LEGACY_FIELDS | { + CONF_VALUE_TEMPLATE: CONF_STATE, + CONF_POSITION_TEMPLATE: CONF_POSITION, + CONF_TILT_TEMPLATE: CONF_TILT, +} + +DEFAULT_NAME = "Template Cover" + COVER_SCHEMA = vol.All( + vol.Schema( + { + vol.Inclusive(CLOSE_ACTION, CONF_OPEN_AND_CLOSE): cv.SCRIPT_SCHEMA, + vol.Inclusive(OPEN_ACTION, CONF_OPEN_AND_CLOSE): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.template, + vol.Optional(CONF_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_PICTURE): cv.template, + vol.Optional(CONF_POSITION): cv.template, + vol.Optional(CONF_STATE): cv.template, + vol.Optional(CONF_TILT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_TILT): cv.template, + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(POSITION_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(STOP_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(TILT_ACTION): cv.SCRIPT_SCHEMA, + } + ) + .extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema) + .extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema), + cv.has_at_least_one_key(OPEN_ACTION, POSITION_ACTION), +) + +LEGACY_COVER_SCHEMA = vol.All( cv.deprecated(CONF_ENTITY_ID), vol.Schema( { @@ -98,29 +137,56 @@ COVER_SCHEMA = vol.All( ) PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend( - {vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA)} + {vol.Required(CONF_COVERS): cv.schema_with_slug_keys(LEGACY_COVER_SCHEMA)} ) -async def _async_create_entities(hass: HomeAssistant, config): - """Create the Template cover.""" +def rewrite_legacy_to_modern_conf( + hass: HomeAssistant, config: dict[str, dict] +) -> list[dict]: + """Rewrite legacy switch configuration definitions to modern ones.""" covers = [] - for object_id, entity_config in config[CONF_COVERS].items(): - entity_config = rewrite_common_legacy_to_modern_conf(hass, entity_config) + for object_id, entity_conf in config.items(): + entity_conf = {**entity_conf, CONF_OBJECT_ID: object_id} - unique_id = entity_config.get(CONF_UNIQUE_ID) + entity_conf = rewrite_common_legacy_to_modern_conf( + hass, entity_conf, LEGACY_FIELDS + ) + + if CONF_NAME not in entity_conf: + entity_conf[CONF_NAME] = template.Template(object_id, hass) + + covers.append(entity_conf) + + return covers + + +@callback +def _async_create_template_tracking_entities( + async_add_entities: AddEntitiesCallback, + hass: HomeAssistant, + definitions: list[dict], + unique_id_prefix: str | None, +) -> None: + """Create the template switches.""" + covers = [] + + for entity_conf in definitions: + unique_id = entity_conf.get(CONF_UNIQUE_ID) + + if unique_id and unique_id_prefix: + unique_id = f"{unique_id_prefix}-{unique_id}" covers.append( CoverTemplate( hass, - object_id, - entity_config, + entity_conf, unique_id, ) ) - return covers + async_add_entities(covers) async def async_setup_platform( @@ -130,7 +196,21 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Template cover.""" - async_add_entities(await _async_create_entities(hass, config)) + if discovery_info is None: + _async_create_template_tracking_entities( + async_add_entities, + hass, + rewrite_legacy_to_modern_conf(hass, config[CONF_COVERS]), + None, + ) + return + + _async_create_template_tracking_entities( + async_add_entities, + hass, + discovery_info["entities"], + discovery_info["unique_id"], + ) class CoverTemplate(TemplateEntity, CoverEntity): @@ -141,23 +221,22 @@ class CoverTemplate(TemplateEntity, CoverEntity): def __init__( self, hass: HomeAssistant, - object_id, config: dict[str, Any], unique_id, ) -> None: """Initialize the Template cover.""" - super().__init__( - hass, config=config, fallback_name=object_id, unique_id=unique_id - ) - self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, object_id, hass=hass - ) + super().__init__(hass, config=config, fallback_name=None, unique_id=unique_id) + if (object_id := config.get(CONF_OBJECT_ID)) is not None: + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, object_id, hass=hass + ) name = self._attr_name if TYPE_CHECKING: assert name is not None - self._template = config.get(CONF_VALUE_TEMPLATE) - self._position_template = config.get(CONF_POSITION_TEMPLATE) - self._tilt_template = config.get(CONF_TILT_TEMPLATE) + self._template = config.get(CONF_STATE) + + self._position_template = config.get(CONF_POSITION) + self._tilt_template = config.get(CONF_TILT) self._attr_device_class = config.get(CONF_DEVICE_CLASS) # The config requires (open and close scripts) or a set position script, diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index 668592e388b..5f28a977867 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -4,7 +4,7 @@ from typing import Any import pytest -from homeassistant import setup +from homeassistant.components import cover, template from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, @@ -29,658 +29,776 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component +from .conftest import ConfigurationStyle + from tests.common import assert_setup_component -ENTITY_COVER = "cover.test_template_cover" +TEST_OBJECT_ID = "test_template_cover" +TEST_ENTITY_ID = f"cover.{TEST_OBJECT_ID}" +TEST_STATE_ENTITY_ID = "cover.test_state" - -OPEN_CLOSE_COVER_CONFIG = { - "open_cover": { - "service": "test.automation", - "data_template": { - "action": "open_cover", - "caller": "{{ this.entity_id }}", - }, - }, - "close_cover": { - "service": "test.automation", - "data_template": { - "action": "close_cover", - "caller": "{{ this.entity_id }}", - }, +OPEN_COVER = { + "service": "test.automation", + "data_template": { + "action": "open_cover", + "caller": "{{ this.entity_id }}", }, } +CLOSE_COVER = { + "service": "test.automation", + "data_template": { + "action": "close_cover", + "caller": "{{ this.entity_id }}", + }, +} -@pytest.mark.parametrize(("count", "domain"), [(1, COVER_DOMAIN)]) -@pytest.mark.parametrize( - ("config", "states"), - [ - ( - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - **OPEN_CLOSE_COVER_CONFIG, - "value_template": "{{ states.cover.test_state.state }}", - } - }, - } - }, - [ - ("cover.test_state", CoverState.OPEN, CoverState.OPEN, {}, -1, ""), - ("cover.test_state", CoverState.CLOSED, CoverState.CLOSED, {}, -1, ""), - ( - "cover.test_state", - CoverState.OPENING, - CoverState.OPENING, - {}, - -1, - "", - ), - ( - "cover.test_state", - CoverState.CLOSING, - CoverState.CLOSING, - {}, - -1, - "", - ), - ( - "cover.test_state", - "dog", - STATE_UNKNOWN, - {}, - -1, - "Received invalid cover is_on state: dog", - ), - ("cover.test_state", CoverState.OPEN, CoverState.OPEN, {}, -1, ""), - ( - "cover.test_state", - "cat", - STATE_UNKNOWN, - {}, - -1, - "Received invalid cover is_on state: cat", - ), - ("cover.test_state", CoverState.CLOSED, CoverState.CLOSED, {}, -1, ""), - ( - "cover.test_state", - "bear", - STATE_UNKNOWN, - {}, - -1, - "Received invalid cover is_on state: bear", - ), - ], - ), - ( - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - **OPEN_CLOSE_COVER_CONFIG, - "position_template": ( - "{{ states.cover.test.attributes.position }}" - ), - "value_template": "{{ states.cover.test_state.state }}", - } - }, - } - }, - [ - ("cover.test_state", CoverState.OPEN, STATE_UNKNOWN, {}, -1, ""), - ("cover.test_state", CoverState.CLOSED, STATE_UNKNOWN, {}, -1, ""), - ( - "cover.test_state", - CoverState.OPENING, - CoverState.OPENING, - {}, - -1, - "", - ), - ( - "cover.test_state", - CoverState.CLOSING, - CoverState.CLOSING, - {}, - -1, - "", - ), - ( - "cover.test", - CoverState.CLOSED, - CoverState.CLOSING, - {"position": 0}, - 0, - "", - ), - ("cover.test_state", CoverState.OPEN, CoverState.CLOSED, {}, -1, ""), - ( - "cover.test", - CoverState.CLOSED, - CoverState.OPEN, - {"position": 10}, - 10, - "", - ), - ( - "cover.test_state", - "dog", - CoverState.OPEN, - {}, - -1, - "Received invalid cover is_on state: dog", - ), - ], - ), - ], -) -@pytest.mark.usefixtures("start_ha") -async def test_template_state_text( - hass: HomeAssistant, states, caplog: pytest.LogCaptureFixture +SET_COVER_POSITION = { + "service": "test.automation", + "data_template": { + "action": "set_cover_position", + "caller": "{{ this.entity_id }}", + "position": "{{ position }}", + }, +} + +SET_COVER_TILT_POSITION = { + "service": "test.automation", + "data_template": { + "action": "set_cover_tilt_position", + "caller": "{{ this.entity_id }}", + "tilt_position": "{{ tilt }}", + }, +} + +COVER_ACTIONS = { + "open_cover": OPEN_COVER, + "close_cover": CLOSE_COVER, +} +NAMED_COVER_ACTIONS = { + **COVER_ACTIONS, + "name": TEST_OBJECT_ID, +} +UNIQUE_ID_CONFIG = { + **COVER_ACTIONS, + "unique_id": "not-so-unique-anymore", +} + + +async def async_setup_legacy_format( + hass: HomeAssistant, count: int, cover_config: dict[str, Any] ) -> None: - """Test the state text of a template.""" - state = hass.states.get("cover.test_template_cover") - assert state.state == STATE_UNKNOWN + """Do setup of cover integration via legacy format.""" + config = {"cover": {"platform": "template", "covers": cover_config}} - for entity, set_state, test_state, attr, pos, text in states: - hass.states.async_set(entity, set_state, attributes=attr) - await hass.async_block_till_done() - state = hass.states.get("cover.test_template_cover") - assert state.state == test_state - if pos >= 0: - assert state.attributes.get("current_position") == pos - assert text in caplog.text - - -@pytest.mark.parametrize(("count", "domain"), [(1, COVER_DOMAIN)]) -@pytest.mark.parametrize( - ("config", "entity", "set_state", "test_state", "attr"), - [ - ( - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - **OPEN_CLOSE_COVER_CONFIG, - "position_template": ( - "{{ states.cover.test.attributes.position }}" - ), - "value_template": "{{ states.cover.test_state.state }}", - } - }, - } - }, - "cover.test_state", - "", - STATE_UNKNOWN, - {}, - ), - ( - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - **OPEN_CLOSE_COVER_CONFIG, - "position_template": ( - "{{ states.cover.test.attributes.position }}" - ), - "value_template": "{{ states.cover.test_state.state }}", - } - }, - } - }, - "cover.test_state", - None, - STATE_UNKNOWN, - {}, - ), - ], -) -@pytest.mark.usefixtures("start_ha") -async def test_template_state_text_ignored_if_none_or_empty( - hass: HomeAssistant, - entity: str, - set_state: str, - test_state: str, - attr: dict[str, Any], - caplog: pytest.LogCaptureFixture, -) -> None: - """Test ignoring an empty state text of a template.""" - state = hass.states.get("cover.test_template_cover") - assert state.state == STATE_UNKNOWN - - hass.states.async_set(entity, set_state, attributes=attr) - await hass.async_block_till_done() - state = hass.states.get("cover.test_template_cover") - assert state.state == test_state - assert "ERROR" not in caplog.text - - -@pytest.mark.parametrize(("count", "domain"), [(1, COVER_DOMAIN)]) -@pytest.mark.parametrize( - "config", - [ - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - **OPEN_CLOSE_COVER_CONFIG, - "value_template": "{{ 1 == 1 }}", - } - }, - } - }, - ], -) -@pytest.mark.usefixtures("start_ha") -async def test_template_state_boolean(hass: HomeAssistant) -> None: - """Test the value_template attribute.""" - state = hass.states.get("cover.test_template_cover") - assert state.state == CoverState.OPEN - - -@pytest.mark.parametrize(("count", "domain"), [(1, COVER_DOMAIN)]) -@pytest.mark.parametrize( - "config", - [ - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - **OPEN_CLOSE_COVER_CONFIG, - "position_template": ( - "{{ states.cover.test.attributes.position }}" - ), - } - }, - } - }, - ], -) -@pytest.mark.usefixtures("start_ha") -async def test_template_position( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: - """Test the position_template attribute.""" - hass.states.async_set("cover.test", CoverState.OPEN) - attrs = {} - - for set_state, pos, test_state in ( - (CoverState.CLOSED, 42, CoverState.OPEN), - (CoverState.OPEN, 0.0, CoverState.CLOSED), - (CoverState.CLOSED, None, STATE_UNKNOWN), - ): - attrs["position"] = pos - hass.states.async_set("cover.test", set_state, attributes=attrs) - await hass.async_block_till_done() - state = hass.states.get("cover.test_template_cover") - assert state.attributes.get("current_position") == pos - assert state.state == test_state - assert "ValueError" not in caplog.text - - -@pytest.mark.parametrize(("count", "domain"), [(1, COVER_DOMAIN)]) -@pytest.mark.parametrize( - "config", - [ - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - **OPEN_CLOSE_COVER_CONFIG, - "optimistic": False, - } - }, - } - }, - ], -) -@pytest.mark.usefixtures("start_ha") -async def test_template_not_optimistic(hass: HomeAssistant) -> None: - """Test the is_closed attribute.""" - state = hass.states.get("cover.test_template_cover") - assert state.state == STATE_UNKNOWN - - -@pytest.mark.parametrize(("count", "domain"), [(1, COVER_DOMAIN)]) -@pytest.mark.parametrize( - ("config", "tilt_position"), - [ - ( - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - **OPEN_CLOSE_COVER_CONFIG, - "value_template": "{{ 1 == 1 }}", - "tilt_template": "{{ 42 }}", - } - }, - } - }, - 42.0, - ), - ( - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - **OPEN_CLOSE_COVER_CONFIG, - "value_template": "{{ 1 == 1 }}", - "tilt_template": "{{ None }}", - } - }, - } - }, - None, - ), - ], -) -@pytest.mark.usefixtures("start_ha") -async def test_template_tilt(hass: HomeAssistant, tilt_position: float | None) -> None: - """Test the tilt_template attribute.""" - state = hass.states.get("cover.test_template_cover") - assert state.attributes.get("current_tilt_position") == tilt_position - - -@pytest.mark.parametrize(("count", "domain"), [(1, COVER_DOMAIN)]) -@pytest.mark.parametrize( - "config", - [ - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - **OPEN_CLOSE_COVER_CONFIG, - "position_template": "{{ -1 }}", - "tilt_template": "{{ 110 }}", - } - }, - } - }, - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - **OPEN_CLOSE_COVER_CONFIG, - "position_template": "{{ on }}", - "tilt_template": ( - "{% if states.cover.test_state.state %}" - "on" - "{% else %}" - "off" - "{% endif %}" - ), - }, - }, - } - }, - ], -) -@pytest.mark.usefixtures("start_ha") -async def test_template_out_of_bounds(hass: HomeAssistant) -> None: - """Test template out-of-bounds condition.""" - state = hass.states.get("cover.test_template_cover") - assert state.attributes.get("current_tilt_position") is None - assert state.attributes.get("current_position") is None - - -@pytest.mark.parametrize(("count", "domain"), [(0, COVER_DOMAIN)]) -@pytest.mark.parametrize( - "config", - [ - { - COVER_DOMAIN: { - "platform": "template", - "covers": {"test_template_cover": {"value_template": "{{ 1 == 1 }}"}}, - } - }, - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - "value_template": "{{ 1 == 1 }}", - "open_cover": { - "service": "test.automation", - "data_template": { - "action": "open_cover", - "caller": "{{ this.entity_id }}", - }, - }, - } - }, - } - }, - ], -) -@pytest.mark.usefixtures("start_ha") -async def test_template_open_or_position( - hass: HomeAssistant, caplog_setup_text -) -> None: - """Test that at least one of open_cover or set_position is used.""" - assert hass.states.async_all("cover") == [] - assert "Invalid config for 'cover' from integration 'template'" in caplog_setup_text - - -@pytest.mark.parametrize(("count", "domain"), [(1, COVER_DOMAIN)]) -@pytest.mark.parametrize( - "config", - [ - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - **OPEN_CLOSE_COVER_CONFIG, - "position_template": "{{ 0 }}", - } - }, - } - }, - ], -) -@pytest.mark.usefixtures("start_ha") -async def test_open_action(hass: HomeAssistant, calls: list[ServiceCall]) -> None: - """Test the open_cover command.""" - state = hass.states.get("cover.test_template_cover") - assert state.state == CoverState.CLOSED - - await hass.services.async_call( - COVER_DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True - ) - await hass.async_block_till_done() - - assert len(calls) == 1 - assert calls[0].data["action"] == "open_cover" - assert calls[0].data["caller"] == "cover.test_template_cover" - - -@pytest.mark.parametrize(("count", "domain"), [(1, COVER_DOMAIN)]) -@pytest.mark.parametrize( - "config", - [ - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - **OPEN_CLOSE_COVER_CONFIG, - "position_template": "{{ 100 }}", - "stop_cover": { - "service": "test.automation", - "data_template": { - "action": "stop_cover", - "caller": "{{ this.entity_id }}", - }, - }, - } - }, - } - }, - ], -) -@pytest.mark.usefixtures("start_ha") -async def test_close_stop_action(hass: HomeAssistant, calls: list[ServiceCall]) -> None: - """Test the close-cover and stop_cover commands.""" - state = hass.states.get("cover.test_template_cover") - assert state.state == CoverState.OPEN - - await hass.services.async_call( - COVER_DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True - ) - await hass.async_block_till_done() - - await hass.services.async_call( - COVER_DOMAIN, SERVICE_STOP_COVER, {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True - ) - await hass.async_block_till_done() - - assert len(calls) == 2 - assert calls[0].data["action"] == "close_cover" - assert calls[0].data["caller"] == "cover.test_template_cover" - assert calls[1].data["action"] == "stop_cover" - assert calls[1].data["caller"] == "cover.test_template_cover" - - -@pytest.mark.parametrize(("count", "domain"), [(1, "input_number")]) -@pytest.mark.parametrize( - "config", - [ - {"input_number": {"test": {"min": "0", "max": "100", "initial": "42"}}}, - ], -) -@pytest.mark.usefixtures("start_ha") -async def test_set_position(hass: HomeAssistant, calls: list[ServiceCall]) -> None: - """Test the set_position command.""" - with assert_setup_component(1, "cover"): - assert await setup.async_setup_component( + with assert_setup_component(count, cover.DOMAIN): + assert await async_setup_component( hass, - "cover", - { - "cover": { - "platform": "template", - "covers": { - "test_template_cover": { - "set_cover_position": { - "service": "test.automation", - "data_template": { - "action": "set_cover_position", - "caller": "{{ this.entity_id }}", - "position": "{{ position }}", - }, - }, - } - }, - } - }, + cover.DOMAIN, + config, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + +async def async_setup_modern_format( + hass: HomeAssistant, count: int, cover_config: dict[str, Any] +) -> None: + """Do setup of cover integration via modern format.""" + config = {"template": {"cover": cover_config}} + + with assert_setup_component(count, template.DOMAIN): + assert await async_setup_component( + hass, + template.DOMAIN, + config, ) await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() - state = hass.states.async_set("input_number.test", 42) + +async def async_setup_cover_config( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + cover_config: dict[str, Any], +) -> None: + """Do setup of cover integration.""" + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format(hass, count, cover_config) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format(hass, count, cover_config) + + +@pytest.fixture +async def setup_cover( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + cover_config: dict[str, Any], +) -> None: + """Do setup of cover integration.""" + await async_setup_cover_config(hass, count, style, cover_config) + + +@pytest.fixture +async def setup_state_cover( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + state_template: str, +): + """Do setup of cover integration using a state template.""" + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format( + hass, + count, + { + TEST_OBJECT_ID: { + **COVER_ACTIONS, + "value_template": state_template, + } + }, + ) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format( + hass, + count, + { + **NAMED_COVER_ACTIONS, + "state": state_template, + }, + ) + + +@pytest.fixture +async def setup_position_cover( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + position_template: str, +): + """Do setup of cover integration using a state template.""" + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format( + hass, + count, + { + TEST_OBJECT_ID: { + **COVER_ACTIONS, + "position_template": position_template, + } + }, + ) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format( + hass, + count, + { + **NAMED_COVER_ACTIONS, + "position": position_template, + }, + ) + + +@pytest.fixture +async def setup_single_attribute_state_cover( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + state_template: str, + attribute: str, + attribute_template: str, +) -> None: + """Do setup of cover integration testing a single attribute.""" + extra = {attribute: attribute_template} if attribute and attribute_template else {} + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format( + hass, + count, + { + TEST_OBJECT_ID: { + **COVER_ACTIONS, + "value_template": state_template, + **extra, + } + }, + ) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format( + hass, + count, + { + **NAMED_COVER_ACTIONS, + "state": state_template, + **extra, + }, + ) + + +@pytest.mark.parametrize( + ("count", "state_template"), [(1, "{{ states.cover.test_state.state }}")] +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) +@pytest.mark.parametrize( + ("set_state", "test_state", "text"), + [ + (CoverState.OPEN, CoverState.OPEN, ""), + (CoverState.CLOSED, CoverState.CLOSED, ""), + (CoverState.OPENING, CoverState.OPENING, ""), + (CoverState.CLOSING, CoverState.CLOSING, ""), + ("dog", STATE_UNKNOWN, "Received invalid cover is_on state: dog"), + ("cat", STATE_UNKNOWN, "Received invalid cover is_on state: cat"), + ("bear", STATE_UNKNOWN, "Received invalid cover is_on state: bear"), + ], +) +async def test_template_state_text( + hass: HomeAssistant, + set_state: str, + test_state: str, + text: str, + caplog: pytest.LogCaptureFixture, + setup_state_cover, +) -> None: + """Test the state text of a template.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_UNKNOWN + + hass.states.async_set(TEST_STATE_ENTITY_ID, set_state) await hass.async_block_till_done() - state = hass.states.get("cover.test_template_cover") + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == test_state + assert text in caplog.text + + +@pytest.mark.parametrize( + ("count", "state_template", "attribute_template"), + [ + ( + 1, + "{{ states.cover.test_state.state }}", + "{{ states.cover.test_position.attributes.position }}", + ) + ], +) +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "position_template"), + (ConfigurationStyle.MODERN, "position"), + ], +) +@pytest.mark.parametrize( + "states", + [ + ( + [ + (TEST_STATE_ENTITY_ID, CoverState.OPEN, STATE_UNKNOWN, "", None), + (TEST_STATE_ENTITY_ID, CoverState.CLOSED, STATE_UNKNOWN, "", None), + ( + TEST_STATE_ENTITY_ID, + CoverState.OPENING, + CoverState.OPENING, + "", + None, + ), + ( + TEST_STATE_ENTITY_ID, + CoverState.CLOSING, + CoverState.CLOSING, + "", + None, + ), + ("cover.test_position", CoverState.CLOSED, CoverState.CLOSING, "", 0), + (TEST_STATE_ENTITY_ID, CoverState.OPEN, CoverState.CLOSED, "", None), + ("cover.test_position", CoverState.CLOSED, CoverState.OPEN, "", 10), + ( + TEST_STATE_ENTITY_ID, + "dog", + CoverState.OPEN, + "Received invalid cover is_on state: dog", + None, + ), + ] + ) + ], +) +async def test_template_state_text_with_position( + hass: HomeAssistant, + states: list[tuple[str, str, str, int | None]], + caplog: pytest.LogCaptureFixture, + setup_single_attribute_state_cover, +) -> None: + """Test the state of a position template in order.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_UNKNOWN + + for test_entity, set_state, test_state, text, position in states: + attrs = {"position": position} if position is not None else {} + + hass.states.async_set(test_entity, set_state, attrs) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == test_state + if position is not None: + assert state.attributes.get("current_position") == position + assert text in caplog.text + + +@pytest.mark.parametrize( + ("count", "state_template", "attribute_template"), + [ + ( + 1, + "{{ states.cover.test_state.state }}", + "{{ states.cover.test_position.attributes.position }}", + ) + ], +) +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "position_template"), + (ConfigurationStyle.MODERN, "position"), + ], +) +@pytest.mark.parametrize( + "set_state", + [ + "", + None, + ], +) +async def test_template_state_text_ignored_if_none_or_empty( + hass: HomeAssistant, + set_state: str, + caplog: pytest.LogCaptureFixture, + setup_single_attribute_state_cover, +) -> None: + """Test ignoring an empty state text of a template.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_UNKNOWN + + hass.states.async_set(TEST_STATE_ENTITY_ID, set_state) + await hass.async_block_till_done() + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_UNKNOWN + assert "ERROR" not in caplog.text + + +@pytest.mark.parametrize(("count", "state_template"), [(1, "{{ 1 == 1 }}")]) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) +async def test_template_state_boolean(hass: HomeAssistant, setup_state_cover) -> None: + """Test the value_template attribute.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == CoverState.OPEN + + +@pytest.mark.parametrize( + ("count", "position_template"), + [(1, "{{ states.cover.test_state.attributes.position }}")], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) +@pytest.mark.parametrize( + ("test_state", "position", "expected"), + [ + (CoverState.CLOSED, 42, CoverState.OPEN), + (CoverState.OPEN, 0.0, CoverState.CLOSED), + (CoverState.CLOSED, None, STATE_UNKNOWN), + ], +) +async def test_template_position( + hass: HomeAssistant, + test_state: str, + position: int | None, + expected: str, + caplog: pytest.LogCaptureFixture, + setup_position_cover, +) -> None: + """Test the position_template attribute.""" + hass.states.async_set(TEST_STATE_ENTITY_ID, CoverState.OPEN) + await hass.async_block_till_done() + + hass.states.async_set( + TEST_STATE_ENTITY_ID, test_state, attributes={"position": position} + ) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get("current_position") == position + assert state.state == expected + assert "ValueError" not in caplog.text + + +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + ("style", "cover_config"), + [ + ( + ConfigurationStyle.LEGACY, + { + "test_template_cover": { + **COVER_ACTIONS, + "optimistic": False, + } + }, + ), + ( + ConfigurationStyle.MODERN, + { + **NAMED_COVER_ACTIONS, + "optimistic": False, + }, + ), + ], +) +async def test_template_not_optimistic(hass: HomeAssistant, setup_cover) -> None: + """Test the is_closed attribute.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_UNKNOWN + + +@pytest.mark.parametrize(("count", "state_template"), [(1, "{{ 1 == 1 }}")]) +@pytest.mark.parametrize( + ("style", "attribute"), + [ + ( + ConfigurationStyle.LEGACY, + "tilt_template", + ), + ( + ConfigurationStyle.MODERN, + "tilt", + ), + ], +) +@pytest.mark.parametrize( + ("attribute_template", "tilt_position"), + [ + ("{{ 1 }}", 1.0), + ("{{ 42 }}", 42.0), + ("{{ 100 }}", 100.0), + ("{{ None }}", None), + ("{{ 110 }}", None), + ("{{ -1 }}", None), + ("{{ 'on' }}", None), + ], +) +async def test_template_tilt( + hass: HomeAssistant, tilt_position: float | None, setup_single_attribute_state_cover +) -> None: + """Test tilt in and out-of-bound conditions.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get("current_tilt_position") == tilt_position + + +@pytest.mark.parametrize(("count", "state_template"), [(1, "{{ 1 == 1 }}")]) +@pytest.mark.parametrize( + ("style", "attribute"), + [ + ( + ConfigurationStyle.LEGACY, + "position_template", + ), + ( + ConfigurationStyle.MODERN, + "position", + ), + ], +) +@pytest.mark.parametrize( + "attribute_template", + [ + "{{ -1 }}", + "{{ 110 }}", + "{{ 'on' }}", + "{{ 'off' }}", + ], +) +async def test_position_out_of_bounds( + hass: HomeAssistant, setup_single_attribute_state_cover +) -> None: + """Test position out-of-bounds condition.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get("current_position") is None + + +@pytest.mark.parametrize("count", [0]) +@pytest.mark.parametrize( + ("style", "cover_config", "error"), + [ + ( + ConfigurationStyle.LEGACY, + { + "test_template_cover": { + "value_template": "{{ 1 == 1 }}", + } + }, + "Invalid config for 'cover' from integration 'template'", + ), + ( + ConfigurationStyle.LEGACY, + { + "test_template_cover": { + "value_template": "{{ 1 == 1 }}", + "open_cover": OPEN_COVER, + } + }, + "Invalid config for 'cover' from integration 'template'", + ), + ( + ConfigurationStyle.MODERN, + { + "name": TEST_OBJECT_ID, + "state": "{{ 1 == 1 }}", + }, + "Invalid config for 'template': must contain at least one of open_cover, set_cover_position.", + ), + ( + ConfigurationStyle.MODERN, + { + "name": TEST_OBJECT_ID, + "state": "{{ 1 == 1 }}", + "open_cover": OPEN_COVER, + }, + "Invalid config for 'template': some but not all values in the same group of inclusion 'open_or_close'", + ), + ], +) +async def test_template_open_or_position( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + cover_config: dict[str, Any], + error: str, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that at least one of open_cover or set_position is used.""" + await async_setup_cover_config(hass, count, style, cover_config) + assert hass.states.async_all("cover") == [] + assert error in caplog.text + + +@pytest.mark.parametrize( + ("count", "position_template"), + [(1, "{{ 0 }}")], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) +async def test_open_action( + hass: HomeAssistant, setup_position_cover, calls: list[ServiceCall] +) -> None: + """Test the open_cover command.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == CoverState.CLOSED + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["action"] == "open_cover" + assert calls[0].data["caller"] == TEST_ENTITY_ID + + +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + ("style", "cover_config"), + [ + ( + ConfigurationStyle.LEGACY, + { + "test_template_cover": { + **COVER_ACTIONS, + "position_template": "{{ 100 }}", + "stop_cover": { + "service": "test.automation", + "data_template": { + "action": "stop_cover", + "caller": "{{ this.entity_id }}", + }, + }, + } + }, + ), + ( + ConfigurationStyle.MODERN, + { + **NAMED_COVER_ACTIONS, + "position": "{{ 100 }}", + "stop_cover": { + "service": "test.automation", + "data_template": { + "action": "stop_cover", + "caller": "{{ this.entity_id }}", + }, + }, + }, + ), + ], +) +async def test_close_stop_action( + hass: HomeAssistant, setup_cover, calls: list[ServiceCall] +) -> None: + """Test the close-cover and stop_cover commands.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == CoverState.OPEN + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + assert len(calls) == 2 + assert calls[0].data["action"] == "close_cover" + assert calls[0].data["caller"] == TEST_ENTITY_ID + assert calls[1].data["action"] == "stop_cover" + assert calls[1].data["caller"] == TEST_ENTITY_ID + + +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + ("style", "cover_config"), + [ + ( + ConfigurationStyle.LEGACY, + { + "test_template_cover": { + "set_cover_position": SET_COVER_POSITION, + } + }, + ), + ( + ConfigurationStyle.MODERN, + { + "name": TEST_OBJECT_ID, + "set_cover_position": SET_COVER_POSITION, + }, + ), + ], +) +async def test_set_position( + hass: HomeAssistant, setup_cover, calls: list[ServiceCall] +) -> None: + """Test the set_position command.""" + state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_UNKNOWN await hass.services.async_call( - COVER_DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, + blocking=True, ) await hass.async_block_till_done() - state = hass.states.get("cover.test_template_cover") + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes.get("current_position") == 100.0 assert len(calls) == 1 assert calls[-1].data["action"] == "set_cover_position" - assert calls[-1].data["caller"] == "cover.test_template_cover" + assert calls[-1].data["caller"] == TEST_ENTITY_ID assert calls[-1].data["position"] == 100 await hass.services.async_call( - COVER_DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, + blocking=True, ) await hass.async_block_till_done() - state = hass.states.get("cover.test_template_cover") + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes.get("current_position") == 0.0 assert len(calls) == 2 assert calls[-1].data["action"] == "set_cover_position" - assert calls[-1].data["caller"] == "cover.test_template_cover" + assert calls[-1].data["caller"] == TEST_ENTITY_ID assert calls[-1].data["position"] == 0 await hass.services.async_call( - COVER_DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True + COVER_DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True ) await hass.async_block_till_done() - state = hass.states.get("cover.test_template_cover") + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes.get("current_position") == 100.0 assert len(calls) == 3 assert calls[-1].data["action"] == "set_cover_position" - assert calls[-1].data["caller"] == "cover.test_template_cover" + assert calls[-1].data["caller"] == TEST_ENTITY_ID assert calls[-1].data["position"] == 100 await hass.services.async_call( - COVER_DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True + COVER_DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True ) await hass.async_block_till_done() - state = hass.states.get("cover.test_template_cover") + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes.get("current_position") == 0.0 assert len(calls) == 4 assert calls[-1].data["action"] == "set_cover_position" - assert calls[-1].data["caller"] == "cover.test_template_cover" + assert calls[-1].data["caller"] == TEST_ENTITY_ID assert calls[-1].data["position"] == 0 await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_POSITION, - {ATTR_ENTITY_ID: ENTITY_COVER, ATTR_POSITION: 25}, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_POSITION: 25}, blocking=True, ) await hass.async_block_till_done() - state = hass.states.get("cover.test_template_cover") + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes.get("current_position") == 25.0 assert len(calls) == 5 assert calls[-1].data["action"] == "set_cover_position" - assert calls[-1].data["caller"] == "cover.test_template_cover" + assert calls[-1].data["caller"] == TEST_ENTITY_ID assert calls[-1].data["position"] == 25 -@pytest.mark.parametrize(("count", "domain"), [(1, COVER_DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "config", + ("style", "cover_config"), [ - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - **OPEN_CLOSE_COVER_CONFIG, - "set_cover_tilt_position": { - "service": "test.automation", - "data_template": { - "action": "set_cover_tilt_position", - "caller": "{{ this.entity_id }}", - "tilt_position": "{{ tilt }}", - }, - }, - } - }, - } - }, + ( + ConfigurationStyle.LEGACY, + { + "test_template_cover": { + **COVER_ACTIONS, + "set_cover_tilt_position": SET_COVER_TILT_POSITION, + } + }, + ), + ( + ConfigurationStyle.MODERN, + { + **NAMED_COVER_ACTIONS, + "set_cover_tilt_position": SET_COVER_TILT_POSITION, + }, + ), ], ) @pytest.mark.parametrize( @@ -688,20 +806,20 @@ async def test_set_position(hass: HomeAssistant, calls: list[ServiceCall]) -> No [ ( SERVICE_SET_COVER_TILT_POSITION, - {ATTR_ENTITY_ID: ENTITY_COVER, ATTR_TILT_POSITION: 42}, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_TILT_POSITION: 42}, 42, ), - (SERVICE_OPEN_COVER_TILT, {ATTR_ENTITY_ID: ENTITY_COVER}, 100), - (SERVICE_CLOSE_COVER_TILT, {ATTR_ENTITY_ID: ENTITY_COVER}, 0), + (SERVICE_OPEN_COVER_TILT, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, 100), + (SERVICE_CLOSE_COVER_TILT, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, 0), ], ) -@pytest.mark.usefixtures("start_ha") async def test_set_tilt_position( hass: HomeAssistant, service, attr, - calls: list[ServiceCall], tilt_position, + setup_cover, + calls: list[ServiceCall], ) -> None: """Test the set_tilt_position command.""" await hass.services.async_call( @@ -714,42 +832,46 @@ async def test_set_tilt_position( assert len(calls) == 1 assert calls[-1].data["action"] == "set_cover_tilt_position" - assert calls[-1].data["caller"] == "cover.test_template_cover" + assert calls[-1].data["caller"] == TEST_ENTITY_ID assert calls[-1].data["tilt_position"] == tilt_position -@pytest.mark.parametrize(("count", "domain"), [(1, COVER_DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "config", + ("style", "cover_config"), [ - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - "set_cover_position": {"service": "test.automation"} - } - }, - } - }, + ( + ConfigurationStyle.LEGACY, + { + "test_template_cover": { + "set_cover_position": SET_COVER_POSITION, + } + }, + ), + ( + ConfigurationStyle.MODERN, + { + "name": TEST_OBJECT_ID, + "set_cover_position": SET_COVER_POSITION, + }, + ), ], ) -@pytest.mark.usefixtures("start_ha") async def test_set_position_optimistic( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, setup_cover, calls: list[ServiceCall] ) -> None: """Test optimistic position mode.""" - state = hass.states.get("cover.test_template_cover") + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes.get("current_position") is None await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_POSITION, - {ATTR_ENTITY_ID: ENTITY_COVER, ATTR_POSITION: 42}, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_POSITION: 42}, blocking=True, ) await hass.async_block_till_done() - state = hass.states.get("cover.test_template_cover") + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes.get("current_position") == 42.0 for service, test_state in ( @@ -759,47 +881,53 @@ async def test_set_position_optimistic( (SERVICE_TOGGLE, CoverState.OPEN), ): await hass.services.async_call( - COVER_DOMAIN, service, {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True + COVER_DOMAIN, service, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True ) await hass.async_block_till_done() - state = hass.states.get("cover.test_template_cover") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == test_state -@pytest.mark.parametrize(("count", "domain"), [(1, COVER_DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "config", + ("style", "cover_config"), [ - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - "position_template": "{{ 100 }}", - "set_cover_position": {"service": "test.automation"}, - "set_cover_tilt_position": {"service": "test.automation"}, - } - }, - } - }, + ( + ConfigurationStyle.LEGACY, + { + "test_template_cover": { + "position_template": "{{ 100 }}", + "set_cover_position": SET_COVER_POSITION, + "set_cover_tilt_position": SET_COVER_TILT_POSITION, + } + }, + ), + ( + ConfigurationStyle.MODERN, + { + "name": TEST_OBJECT_ID, + "position": "{{ 100 }}", + "set_cover_position": SET_COVER_POSITION, + "set_cover_tilt_position": SET_COVER_TILT_POSITION, + }, + ), ], ) -@pytest.mark.usefixtures("calls", "start_ha") async def test_set_tilt_position_optimistic( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, setup_cover, calls: list[ServiceCall] ) -> None: """Test the optimistic tilt_position mode.""" - state = hass.states.get("cover.test_template_cover") + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes.get("current_tilt_position") is None await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_TILT_POSITION, - {ATTR_ENTITY_ID: ENTITY_COVER, ATTR_TILT_POSITION: 42}, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_TILT_POSITION: 42}, blocking=True, ) await hass.async_block_till_done() - state = hass.states.get("cover.test_template_cover") + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes.get("current_tilt_position") == 42.0 for service, pos in ( @@ -809,157 +937,140 @@ async def test_set_tilt_position_optimistic( (SERVICE_TOGGLE_COVER_TILT, 100.0), ): await hass.services.async_call( - COVER_DOMAIN, service, {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True + COVER_DOMAIN, service, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True ) await hass.async_block_till_done() - state = hass.states.get("cover.test_template_cover") + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes.get("current_tilt_position") == pos -@pytest.mark.parametrize(("count", "domain"), [(1, COVER_DOMAIN)]) @pytest.mark.parametrize( - "config", + ("count", "state_template", "attribute_template"), [ - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - **OPEN_CLOSE_COVER_CONFIG, - "value_template": "{{ states.cover.test_state.state }}", - "icon_template": ( - "{% if states.cover.test_state.state %}mdi:check{% endif %}" - ), - } - }, - } - }, + ( + 1, + "{{ states.cover.test_state.state }}", + "{% if states.cover.test_state.state %}mdi:check{% endif %}", + ) ], ) -@pytest.mark.usefixtures("start_ha") -async def test_icon_template(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "icon_template"), + (ConfigurationStyle.MODERN, "icon"), + ], +) +async def test_icon_template( + hass: HomeAssistant, setup_single_attribute_state_cover +) -> None: """Test icon template.""" - state = hass.states.get("cover.test_template_cover") + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes.get("icon") == "" state = hass.states.async_set("cover.test_state", CoverState.OPEN) await hass.async_block_till_done() - state = hass.states.get("cover.test_template_cover") + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes["icon"] == "mdi:check" -@pytest.mark.parametrize(("count", "domain"), [(1, COVER_DOMAIN)]) @pytest.mark.parametrize( - "config", + ("count", "state_template", "attribute_template"), [ - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - **OPEN_CLOSE_COVER_CONFIG, - "value_template": "{{ states.cover.test_state.state }}", - "entity_picture_template": ( - "{% if states.cover.test_state.state %}" - "/local/cover.png" - "{% endif %}" - ), - } - }, - } - }, + ( + 1, + "{{ states.cover.test_state.state }}", + "{% if states.cover.test_state.state %}/local/cover.png{% endif %}", + ) ], ) -@pytest.mark.usefixtures("start_ha") -async def test_entity_picture_template(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "entity_picture_template"), + (ConfigurationStyle.MODERN, "picture"), + ], +) +async def test_entity_picture_template( + hass: HomeAssistant, setup_single_attribute_state_cover +) -> None: """Test icon template.""" - state = hass.states.get("cover.test_template_cover") + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes.get("entity_picture") == "" state = hass.states.async_set("cover.test_state", CoverState.OPEN) await hass.async_block_till_done() - state = hass.states.get("cover.test_template_cover") + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes["entity_picture"] == "/local/cover.png" -@pytest.mark.parametrize(("count", "domain"), [(1, COVER_DOMAIN)]) @pytest.mark.parametrize( - "config", + ("count", "state_template", "attribute_template"), [ - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - **OPEN_CLOSE_COVER_CONFIG, - "value_template": "open", - "availability_template": ( - "{{ is_state('availability_state.state','on') }}" - ), - } - }, - } - }, + ( + 1, + "{{ 1 == 1 }}", + "{{ is_state('availability_state.state','on') }}", + ) ], ) -@pytest.mark.usefixtures("start_ha") -async def test_availability_template(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "availability_template"), + (ConfigurationStyle.MODERN, "availability"), + ], +) +async def test_availability_template( + hass: HomeAssistant, setup_single_attribute_state_cover +) -> None: """Test availability template.""" hass.states.async_set("availability_state.state", STATE_OFF) await hass.async_block_till_done() - assert hass.states.get("cover.test_template_cover").state == STATE_UNAVAILABLE + assert hass.states.get(TEST_ENTITY_ID).state == STATE_UNAVAILABLE hass.states.async_set("availability_state.state", STATE_ON) await hass.async_block_till_done() - assert hass.states.get("cover.test_template_cover").state != STATE_UNAVAILABLE + assert hass.states.get(TEST_ENTITY_ID).state != STATE_UNAVAILABLE -@pytest.mark.parametrize(("count", "domain"), [(1, COVER_DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "config", + ("config", "domain"), [ - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - **OPEN_CLOSE_COVER_CONFIG, - "value_template": "open", - } - }, - } - }, - ], -) -@pytest.mark.usefixtures("start_ha") -async def test_availability_without_availability_template(hass: HomeAssistant) -> None: - """Test that component is available if there is no.""" - state = hass.states.get("cover.test_template_cover") - assert state.state != STATE_UNAVAILABLE - - -@pytest.mark.parametrize(("count", "domain"), [(1, COVER_DOMAIN)]) -@pytest.mark.parametrize( - "config", - [ - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - **OPEN_CLOSE_COVER_CONFIG, - "availability_template": "{{ x - 12 }}", - "value_template": "open", - } - }, - } - }, + ( + { + COVER_DOMAIN: { + "platform": "template", + "covers": { + "test_template_cover": { + **COVER_ACTIONS, + "availability_template": "{{ x - 12 }}", + "value_template": "open", + } + }, + } + }, + cover.DOMAIN, + ), + ( + { + "template": { + "cover": { + **NAMED_COVER_ACTIONS, + "state": "{{ true }}", + "availability": "{{ x - 12 }}", + }, + } + }, + template.DOMAIN, + ), ], ) @pytest.mark.usefixtures("start_ha") @@ -967,111 +1078,142 @@ async def test_invalid_availability_template_keeps_component_available( hass: HomeAssistant, caplog_setup_text ) -> None: """Test that an invalid availability keeps the device available.""" - assert hass.states.get("cover.test_template_cover") != STATE_UNAVAILABLE + assert hass.states.get(TEST_ENTITY_ID) != STATE_UNAVAILABLE assert "UndefinedError: 'x' is undefined" in caplog_setup_text -@pytest.mark.parametrize(("count", "domain"), [(1, COVER_DOMAIN)]) @pytest.mark.parametrize( - "config", - [ - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - **OPEN_CLOSE_COVER_CONFIG, - "value_template": "{{ states.cover.test_state.state }}", - "device_class": "door", - } - }, - } - }, - ], + ("count", "state_template", "attribute", "attribute_template"), + [(1, "{{ 1 == 1 }}", "device_class", "door")], ) -@pytest.mark.usefixtures("start_ha") -async def test_device_class(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN], +) +async def test_device_class( + hass: HomeAssistant, setup_single_attribute_state_cover +) -> None: """Test device class.""" - state = hass.states.get("cover.test_template_cover") + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes.get("device_class") == "door" -@pytest.mark.parametrize(("count", "domain"), [(0, COVER_DOMAIN)]) @pytest.mark.parametrize( - "config", - [ - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - **OPEN_CLOSE_COVER_CONFIG, - "value_template": "{{ states.cover.test_state.state }}", - "device_class": "barnacle_bill", - } - }, - } - }, - ], + ("count", "state_template", "attribute", "attribute_template"), + [(0, "{{ 1 == 1 }}", "device_class", "barnacle_bill")], ) -@pytest.mark.usefixtures("start_ha") -async def test_invalid_device_class(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN], +) +async def test_invalid_device_class( + hass: HomeAssistant, setup_single_attribute_state_cover +) -> None: """Test device class.""" - state = hass.states.get("cover.test_template_cover") + state = hass.states.get(TEST_ENTITY_ID) assert not state -@pytest.mark.parametrize(("count", "domain"), [(1, COVER_DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "config", + ("cover_config", "style"), [ - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover_01": { - **OPEN_CLOSE_COVER_CONFIG, - "unique_id": "not-so-unique-anymore", - "value_template": "{{ true }}", - }, - "test_template_cover_02": { - **OPEN_CLOSE_COVER_CONFIG, - "unique_id": "not-so-unique-anymore", - "value_template": "{{ false }}", - }, + ( + { + "test_template_cover_01": UNIQUE_ID_CONFIG, + "test_template_cover_02": UNIQUE_ID_CONFIG, + }, + ConfigurationStyle.LEGACY, + ), + ( + [ + { + "name": "test_template_cover_01", + **UNIQUE_ID_CONFIG, }, - } - }, + { + "name": "test_template_cover_02", + **UNIQUE_ID_CONFIG, + }, + ], + ConfigurationStyle.MODERN, + ), ], ) -@pytest.mark.usefixtures("start_ha") -async def test_unique_id(hass: HomeAssistant) -> None: +async def test_unique_id(hass: HomeAssistant, setup_cover) -> None: """Test unique_id option only creates one cover per id.""" assert len(hass.states.async_all()) == 1 -@pytest.mark.parametrize(("count", "domain"), [(1, COVER_DOMAIN)]) -@pytest.mark.parametrize( - "config", - [ - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "garage_door": { - **OPEN_CLOSE_COVER_CONFIG, - "friendly_name": "Garage Door", - "value_template": ( - "{{ is_state('binary_sensor.garage_door_sensor', 'off') }}" - ), - }, +async def test_nested_unique_id( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test a template unique_id propagates to switch unique_ids.""" + with assert_setup_component(1, template.DOMAIN): + assert await async_setup_component( + hass, + template.DOMAIN, + { + "template": { + "unique_id": "x", + "cover": [ + { + **COVER_ACTIONS, + "name": "test_a", + "unique_id": "a", + "state": "{{ true }}", + }, + { + **COVER_ACTIONS, + "name": "test_b", + "unique_id": "b", + "state": "{{ true }}", + }, + ], }, - } - }, + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all("cover")) == 2 + + entry = entity_registry.async_get("cover.test_a") + assert entry + assert entry.unique_id == "x-a" + + entry = entity_registry.async_get("cover.test_b") + assert entry + assert entry.unique_id == "x-b" + + +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + ("style", "cover_config"), + [ + ( + ConfigurationStyle.LEGACY, + { + "garage_door": { + **COVER_ACTIONS, + "friendly_name": "Garage Door", + "value_template": "{{ is_state('binary_sensor.garage_door_sensor', 'off') }}", + }, + }, + ), + ( + ConfigurationStyle.MODERN, + { + "name": "Garage Door", + **COVER_ACTIONS, + "state": "{{ is_state('binary_sensor.garage_door_sensor', 'off') }}", + }, + ), ], ) -@pytest.mark.usefixtures("start_ha") -async def test_state_gets_lowercased(hass: HomeAssistant) -> None: +async def test_state_gets_lowercased(hass: HomeAssistant, setup_cover) -> None: """Test True/False is lowercased.""" hass.states.async_set("binary_sensor.garage_door_sensor", "off") @@ -1085,41 +1227,27 @@ async def test_state_gets_lowercased(hass: HomeAssistant) -> None: assert hass.states.get("cover.garage_door").state == CoverState.CLOSED -@pytest.mark.parametrize(("count", "domain"), [(1, COVER_DOMAIN)]) @pytest.mark.parametrize( - "config", + ("count", "state_template", "attribute_template"), [ - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "office": { - "icon_template": """{% if is_state('cover.office', 'open') %} - mdi:window-shutter-open - {% else %} - mdi:window-shutter - {% endif %}""", - "open_cover": { - "service": "switch.turn_on", - "entity_id": "switch.office_blinds_up", - }, - "close_cover": { - "service": "switch.turn_on", - "entity_id": "switch.office_blinds_down", - }, - "stop_cover": { - "service": "switch.turn_on", - "entity_id": "switch.office_blinds_up", - }, - }, - }, - } - }, + ( + 1, + "{{ states.cover.test_state.state }}", + "mdi:window-shutter{{ '-open' if is_state('cover.test_template_cover', 'open') else '' }}", + ) + ], +) +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "icon_template"), + (ConfigurationStyle.MODERN, "icon"), ], ) -@pytest.mark.usefixtures("start_ha") async def test_self_referencing_icon_with_no_template_is_not_a_loop( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + setup_single_attribute_state_cover, + caplog: pytest.LogCaptureFixture, ) -> None: """Test a self referencing icon with no value template is not a loop.""" assert len(hass.states.async_all()) == 1