diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index f1b58ebffa0..e87c9aee989 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -159,7 +159,6 @@ CONFIG_SECTION_SCHEMA = vol.All( ensure_domains_do_not_have_trigger_or_action( DOMAIN_ALARM_CONTROL_PANEL, DOMAIN_BUTTON, - DOMAIN_COVER, DOMAIN_FAN, DOMAIN_LOCK, DOMAIN_VACUUM, diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 1eb80677f7e..0b2009e83e3 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -12,6 +12,7 @@ from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, DEVICE_CLASSES_SCHEMA, + DOMAIN as COVER_DOMAIN, ENTITY_ID_FORMAT, PLATFORM_SCHEMA as COVER_PLATFORM_SCHEMA, CoverEntity, @@ -35,6 +36,7 @@ from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import TriggerUpdateCoordinator from .const import CONF_OBJECT_ID, CONF_PICTURE, DOMAIN from .entity import AbstractTemplateEntity from .template_entity import ( @@ -45,6 +47,7 @@ from .template_entity import ( TemplateEntity, rewrite_common_legacy_to_modern_conf, ) +from .trigger_entity import TriggerEntity _LOGGER = logging.getLogger(__name__) @@ -207,6 +210,13 @@ async def async_setup_platform( ) return + if "coordinator" in discovery_info: + async_add_entities( + TriggerCoverEntity(hass, discovery_info["coordinator"], config) + for config in discovery_info["entities"] + ) + return + _async_create_template_tracking_entities( async_add_entities, hass, @@ -239,7 +249,13 @@ class AbstractTemplateCover(AbstractTemplateEntity, CoverEntity): self._is_closing = False self._tilt_value: int | None = None - def _register_scripts( + # The config requires (open and close scripts) or a set position script, + # therefore the base supported features will always include them. + self._attr_supported_features: CoverEntityFeature = ( + CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE + ) + + def _iterate_scripts( self, config: dict[str, Any] ) -> Generator[tuple[str, Sequence[dict[str, Any]], CoverEntityFeature | int]]: for action_id, supported_feature in ( @@ -459,13 +475,7 @@ class CoverTemplate(TemplateEntity, AbstractTemplateCover): if TYPE_CHECKING: assert name is not None - # The config requires (open and close scripts) or a set position script, - # therefore the base supported features will always include them. - self._attr_supported_features = ( - CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE - ) - - for action_id, action_config, supported_feature in self._register_scripts( + for action_id, action_config, supported_feature in self._iterate_scripts( config ): self.add_script(action_id, action_config, name, DOMAIN) @@ -504,3 +514,62 @@ class CoverTemplate(TemplateEntity, AbstractTemplateCover): return self._update_opening_and_closing(result) + + +class TriggerCoverEntity(TriggerEntity, AbstractTemplateCover): + """Cover entity based on trigger data.""" + + domain = COVER_DOMAIN + + def __init__( + self, + hass: HomeAssistant, + coordinator: TriggerUpdateCoordinator, + config: ConfigType, + ) -> None: + """Initialize the entity.""" + TriggerEntity.__init__(self, hass, coordinator, config) + AbstractTemplateCover.__init__(self, config) + + # Render the _attr_name before initializing TriggerCoverEntity + self._attr_name = name = self._rendered.get(CONF_NAME, DEFAULT_NAME) + + for action_id, action_config, supported_feature in self._iterate_scripts( + config + ): + self.add_script(action_id, action_config, name, DOMAIN) + self._attr_supported_features |= supported_feature + + for key in (CONF_STATE, CONF_POSITION, CONF_TILT): + if isinstance(config.get(key), template.Template): + self._to_render_simple.append(key) + self._parse_result.add(key) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle update of the data.""" + self._process_data() + + if not self.available: + self.async_write_ha_state() + return + + write_ha_state = False + for key, updater in ( + (CONF_STATE, self._update_opening_and_closing), + (CONF_POSITION, self._update_position), + (CONF_TILT, self._update_tilt), + ): + if (rendered := self._rendered.get(key)) is not None: + updater(rendered) + write_ha_state = True + + if not self._optimistic: + self.async_set_context(self.coordinator.data["context"]) + write_ha_state = True + elif self._optimistic and len(self._rendered) > 0: + # In case any non optimistic template + write_ha_state = True + + if write_ha_state: + self.async_write_ha_state() diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index 5f28a977867..48f45d879cd 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -40,6 +40,22 @@ TEST_OBJECT_ID = "test_template_cover" TEST_ENTITY_ID = f"cover.{TEST_OBJECT_ID}" TEST_STATE_ENTITY_ID = "cover.test_state" +TEST_STATE_TRIGGER = { + "trigger": { + "trigger": "state", + "entity_id": [ + "cover.test_state", + "cover.test_position", + "binary_sensor.garage_door_sensor", + ], + }, + "variables": {"triggering_entity": "{{ trigger.entity_id }}"}, + "action": [ + {"event": "action_event", "event_data": {"what": "{{ triggering_entity}}"}} + ], +} + + OPEN_COVER = { "service": "test.automation", "data_template": { @@ -123,6 +139,24 @@ async def async_setup_modern_format( await hass.async_block_till_done() +async def async_setup_trigger_format( + hass: HomeAssistant, count: int, cover_config: dict[str, Any] +) -> None: + """Do setup of cover integration via trigger format.""" + config = {"template": {**TEST_STATE_TRIGGER, "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() + + async def async_setup_cover_config( hass: HomeAssistant, count: int, @@ -134,6 +168,8 @@ async def async_setup_cover_config( await async_setup_legacy_format(hass, count, cover_config) elif style == ConfigurationStyle.MODERN: await async_setup_modern_format(hass, count, cover_config) + elif style == ConfigurationStyle.TRIGGER: + await async_setup_trigger_format(hass, count, cover_config) @pytest.fixture @@ -175,6 +211,15 @@ async def setup_state_cover( "state": state_template, }, ) + elif style == ConfigurationStyle.TRIGGER: + await async_setup_trigger_format( + hass, + count, + { + **NAMED_COVER_ACTIONS, + "state": state_template, + }, + ) @pytest.fixture @@ -205,6 +250,15 @@ async def setup_position_cover( "position": position_template, }, ) + elif style == ConfigurationStyle.TRIGGER: + await async_setup_trigger_format( + hass, + count, + { + **NAMED_COVER_ACTIONS, + "position": position_template, + }, + ) @pytest.fixture @@ -240,13 +294,57 @@ async def setup_single_attribute_state_cover( **extra, }, ) + elif style == ConfigurationStyle.TRIGGER: + await async_setup_trigger_format( + hass, + count, + { + **NAMED_COVER_ACTIONS, + "state": state_template, + **extra, + }, + ) + + +@pytest.fixture +async def setup_empty_action( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + script: str, +): + """Do setup of cover integration using a empty actions template.""" + empty = { + "open_cover": [], + "close_cover": [], + script: [], + } + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format( + hass, + count, + {TEST_OBJECT_ID: empty}, + ) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format( + hass, + count, + {"name": TEST_OBJECT_ID, **empty}, + ) + elif style == ConfigurationStyle.TRIGGER: + await async_setup_trigger_format( + hass, + count, + {"name": TEST_OBJECT_ID, **empty}, + ) @pytest.mark.parametrize( ("count", "state_template"), [(1, "{{ states.cover.test_state.state }}")] ) @pytest.mark.parametrize( - "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] + "style", + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) @pytest.mark.parametrize( ("set_state", "test_state", "text"), @@ -260,13 +358,13 @@ async def setup_single_attribute_state_cover( ("bear", STATE_UNKNOWN, "Received invalid cover is_on state: bear"), ], ) +@pytest.mark.usefixtures("setup_state_cover") 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) @@ -280,6 +378,36 @@ async def test_template_state_text( assert text in caplog.text +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.parametrize( + ("state_template", "expected"), + [ + ("{{ 'open' }}", CoverState.OPEN), + ("{{ 'closed' }}", CoverState.CLOSED), + ("{{ 'opening' }}", CoverState.OPENING), + ("{{ 'closing' }}", CoverState.CLOSING), + ("{{ 'dog' }}", STATE_UNKNOWN), + ("{{ x - 1 }}", STATE_UNAVAILABLE), + ], +) +@pytest.mark.usefixtures("setup_state_cover") +async def test_template_state_states( + hass: HomeAssistant, + expected: str, +) -> None: + """Test state template states.""" + + hass.states.async_set(TEST_STATE_ENTITY_ID, None) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == expected + + @pytest.mark.parametrize( ("count", "state_template", "attribute_template"), [ @@ -295,6 +423,7 @@ async def test_template_state_text( [ (ConfigurationStyle.LEGACY, "position_template"), (ConfigurationStyle.MODERN, "position"), + (ConfigurationStyle.TRIGGER, "position"), ], ) @pytest.mark.parametrize( @@ -332,11 +461,11 @@ async def test_template_state_text( ) ], ) +@pytest.mark.usefixtures("setup_single_attribute_state_cover") 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) @@ -361,7 +490,7 @@ async def test_template_state_text_with_position( ( 1, "{{ states.cover.test_state.state }}", - "{{ states.cover.test_position.attributes.position }}", + "{{ state_attr('cover.test_state', 'position') }}", ) ], ) @@ -370,6 +499,7 @@ async def test_template_state_text_with_position( [ (ConfigurationStyle.LEGACY, "position_template"), (ConfigurationStyle.MODERN, "position"), + (ConfigurationStyle.TRIGGER, "position"), ], ) @pytest.mark.parametrize( @@ -379,11 +509,10 @@ async def test_template_state_text_with_position( None, ], ) +@pytest.mark.usefixtures("setup_single_attribute_state_cover") 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) @@ -393,15 +522,20 @@ async def test_template_state_text_ignored_if_none_or_empty( 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] + "style", + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) -async def test_template_state_boolean(hass: HomeAssistant, setup_state_cover) -> None: +@pytest.mark.usefixtures("setup_state_cover") +async def test_template_state_boolean(hass: HomeAssistant) -> None: """Test the value_template attribute.""" + # This forces a trigger for trigger based entities + hass.states.async_set(TEST_STATE_ENTITY_ID, None) + await hass.async_block_till_done() + state = hass.states.get(TEST_ENTITY_ID) assert state.state == CoverState.OPEN @@ -411,7 +545,8 @@ async def test_template_state_boolean(hass: HomeAssistant, setup_state_cover) -> [(1, "{{ states.cover.test_state.attributes.position }}")], ) @pytest.mark.parametrize( - "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] + "style", + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) @pytest.mark.parametrize( ("test_state", "position", "expected"), @@ -421,13 +556,13 @@ async def test_template_state_boolean(hass: HomeAssistant, setup_state_cover) -> (CoverState.CLOSED, None, STATE_UNKNOWN), ], ) +@pytest.mark.usefixtures("setup_position_cover") 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) @@ -464,9 +599,17 @@ async def test_template_position( "optimistic": False, }, ), + ( + ConfigurationStyle.TRIGGER, + { + **NAMED_COVER_ACTIONS, + "optimistic": False, + }, + ), ], ) -async def test_template_not_optimistic(hass: HomeAssistant, setup_cover) -> None: +@pytest.mark.usefixtures("setup_cover") +async def test_template_not_optimistic(hass: HomeAssistant) -> None: """Test the is_closed attribute.""" state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_UNKNOWN @@ -484,6 +627,10 @@ async def test_template_not_optimistic(hass: HomeAssistant, setup_cover) -> None ConfigurationStyle.MODERN, "tilt", ), + ( + ConfigurationStyle.TRIGGER, + "tilt", + ), ], ) @pytest.mark.parametrize( @@ -498,10 +645,13 @@ async def test_template_not_optimistic(hass: HomeAssistant, setup_cover) -> None ("{{ 'on' }}", None), ], ) -async def test_template_tilt( - hass: HomeAssistant, tilt_position: float | None, setup_single_attribute_state_cover -) -> None: +@pytest.mark.usefixtures("setup_single_attribute_state_cover") +async def test_template_tilt(hass: HomeAssistant, tilt_position: float | None) -> None: """Test tilt in and out-of-bound conditions.""" + # This forces a trigger for trigger based entities + hass.states.async_set(TEST_STATE_ENTITY_ID, None) + await hass.async_block_till_done() + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes.get("current_tilt_position") == tilt_position @@ -518,6 +668,10 @@ async def test_template_tilt( ConfigurationStyle.MODERN, "position", ), + ( + ConfigurationStyle.TRIGGER, + "position", + ), ], ) @pytest.mark.parametrize( @@ -529,10 +683,13 @@ async def test_template_tilt( "{{ 'off' }}", ], ) -async def test_position_out_of_bounds( - hass: HomeAssistant, setup_single_attribute_state_cover -) -> None: +@pytest.mark.usefixtures("setup_single_attribute_state_cover") +async def test_position_out_of_bounds(hass: HomeAssistant) -> None: """Test position out-of-bounds condition.""" + # This forces a trigger for trigger based entities + hass.states.async_set(TEST_STATE_ENTITY_ID, None) + await hass.async_block_till_done() + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes.get("current_position") is None @@ -577,6 +734,23 @@ async def test_position_out_of_bounds( }, "Invalid config for 'template': some but not all values in the same group of inclusion 'open_or_close'", ), + ( + ConfigurationStyle.TRIGGER, + { + "name": TEST_OBJECT_ID, + "state": "{{ 1 == 1 }}", + }, + "Invalid config for 'template': must contain at least one of open_cover, set_cover_position.", + ), + ( + ConfigurationStyle.TRIGGER, + { + "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( @@ -598,12 +772,17 @@ async def test_template_open_or_position( [(1, "{{ 0 }}")], ) @pytest.mark.parametrize( - "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] + "style", + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) -async def test_open_action( - hass: HomeAssistant, setup_position_cover, calls: list[ServiceCall] -) -> None: +@pytest.mark.usefixtures("setup_position_cover") +async def test_open_action(hass: HomeAssistant, calls: list[ServiceCall]) -> None: """Test the open_cover command.""" + + # This forces a trigger for trigger based entities + hass.states.async_set(TEST_STATE_ENTITY_ID, None) + await hass.async_block_till_done() + state = hass.states.get(TEST_ENTITY_ID) assert state.state == CoverState.CLOSED @@ -654,12 +833,29 @@ async def test_open_action( }, }, ), + ( + ConfigurationStyle.TRIGGER, + { + **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: +@pytest.mark.usefixtures("setup_cover") +async def test_close_stop_action(hass: HomeAssistant, calls: list[ServiceCall]) -> None: """Test the close-cover and stop_cover commands.""" + # This forces a trigger for trigger based entities + hass.states.async_set(TEST_STATE_ENTITY_ID, None) + await hass.async_block_till_done() + state = hass.states.get(TEST_ENTITY_ID) assert state.state == CoverState.OPEN @@ -705,11 +901,17 @@ async def test_close_stop_action( "set_cover_position": SET_COVER_POSITION, }, ), + ( + ConfigurationStyle.TRIGGER, + { + "name": TEST_OBJECT_ID, + "set_cover_position": SET_COVER_POSITION, + }, + ), ], ) -async def test_set_position( - hass: HomeAssistant, setup_cover, calls: list[ServiceCall] -) -> None: +@pytest.mark.usefixtures("setup_cover") +async def test_set_position(hass: HomeAssistant, calls: list[ServiceCall]) -> None: """Test the set_position command.""" state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_UNKNOWN @@ -799,6 +1001,13 @@ async def test_set_position( "set_cover_tilt_position": SET_COVER_TILT_POSITION, }, ), + ( + ConfigurationStyle.TRIGGER, + { + **NAMED_COVER_ACTIONS, + "set_cover_tilt_position": SET_COVER_TILT_POSITION, + }, + ), ], ) @pytest.mark.parametrize( @@ -813,12 +1022,12 @@ async def test_set_position( (SERVICE_CLOSE_COVER_TILT, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, 0), ], ) +@pytest.mark.usefixtures("setup_cover") async def test_set_tilt_position( hass: HomeAssistant, service, attr, tilt_position, - setup_cover, calls: list[ServiceCall], ) -> None: """Test the set_tilt_position command.""" @@ -855,10 +1064,18 @@ async def test_set_tilt_position( "set_cover_position": SET_COVER_POSITION, }, ), + ( + ConfigurationStyle.TRIGGER, + { + "name": TEST_OBJECT_ID, + "set_cover_position": SET_COVER_POSITION, + }, + ), ], ) +@pytest.mark.usefixtures("setup_cover") async def test_set_position_optimistic( - hass: HomeAssistant, setup_cover, calls: list[ServiceCall] + hass: HomeAssistant, calls: list[ServiceCall] ) -> None: """Test optimistic position mode.""" state = hass.states.get(TEST_ENTITY_ID) @@ -888,6 +1105,50 @@ async def test_set_position_optimistic( assert state.state == test_state +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + ("style", "cover_config"), + [ + ( + ConfigurationStyle.TRIGGER, + { + "name": TEST_OBJECT_ID, + "set_cover_position": SET_COVER_POSITION, + "picture": "{{ 'foo.png' if is_state('cover.test_state', 'open') else 'bar.png' }}", + }, + ), + ], +) +@pytest.mark.usefixtures("setup_cover") +async def test_non_optimistic_template_with_optimistic_state( + hass: HomeAssistant, calls: list[ServiceCall] +) -> None: + """Test optimistic state with non-optimistic template.""" + state = hass.states.get(TEST_ENTITY_ID) + assert "entity_picture" not in state.attributes + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_POSITION: 42}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == CoverState.OPEN + assert state.attributes["current_position"] == 42.0 + assert "entity_picture" not in state.attributes + + hass.states.async_set(TEST_STATE_ENTITY_ID, CoverState.OPEN) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == CoverState.OPEN + assert state.attributes["current_position"] == 42.0 + assert state.attributes["entity_picture"] == "foo.png" + + @pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( ("style", "cover_config"), @@ -911,10 +1172,20 @@ async def test_set_position_optimistic( "set_cover_tilt_position": SET_COVER_TILT_POSITION, }, ), + ( + ConfigurationStyle.TRIGGER, + { + "name": TEST_OBJECT_ID, + "position": "{{ 100 }}", + "set_cover_position": SET_COVER_POSITION, + "set_cover_tilt_position": SET_COVER_TILT_POSITION, + }, + ), ], ) +@pytest.mark.usefixtures("setup_cover") async def test_set_tilt_position_optimistic( - hass: HomeAssistant, setup_cover, calls: list[ServiceCall] + hass: HomeAssistant, calls: list[ServiceCall] ) -> None: """Test the optimistic tilt_position mode.""" state = hass.states.get(TEST_ENTITY_ID) @@ -955,18 +1226,20 @@ async def test_set_tilt_position_optimistic( ], ) @pytest.mark.parametrize( - ("style", "attribute"), + ("style", "attribute", "initial_expected_state"), [ - (ConfigurationStyle.LEGACY, "icon_template"), - (ConfigurationStyle.MODERN, "icon"), + (ConfigurationStyle.LEGACY, "icon_template", ""), + (ConfigurationStyle.MODERN, "icon", ""), + (ConfigurationStyle.TRIGGER, "icon", None), ], ) +@pytest.mark.usefixtures("setup_single_attribute_state_cover") async def test_icon_template( - hass: HomeAssistant, setup_single_attribute_state_cover + hass: HomeAssistant, initial_expected_state: str | None ) -> None: """Test icon template.""" state = hass.states.get(TEST_ENTITY_ID) - assert state.attributes.get("icon") == "" + assert state.attributes.get("icon") == initial_expected_state state = hass.states.async_set("cover.test_state", CoverState.OPEN) await hass.async_block_till_done() @@ -987,18 +1260,20 @@ async def test_icon_template( ], ) @pytest.mark.parametrize( - ("style", "attribute"), + ("style", "attribute", "initial_expected_state"), [ - (ConfigurationStyle.LEGACY, "entity_picture_template"), - (ConfigurationStyle.MODERN, "picture"), + (ConfigurationStyle.LEGACY, "entity_picture_template", ""), + (ConfigurationStyle.MODERN, "picture", ""), + (ConfigurationStyle.TRIGGER, "picture", None), ], ) +@pytest.mark.usefixtures("setup_single_attribute_state_cover") async def test_entity_picture_template( - hass: HomeAssistant, setup_single_attribute_state_cover + hass: HomeAssistant, initial_expected_state: str | None ) -> None: """Test icon template.""" state = hass.states.get(TEST_ENTITY_ID) - assert state.attributes.get("entity_picture") == "" + assert state.attributes.get("entity_picture") == initial_expected_state state = hass.states.async_set("cover.test_state", CoverState.OPEN) await hass.async_block_till_done() @@ -1023,18 +1298,22 @@ async def test_entity_picture_template( [ (ConfigurationStyle.LEGACY, "availability_template"), (ConfigurationStyle.MODERN, "availability"), + (ConfigurationStyle.TRIGGER, "availability"), ], ) -async def test_availability_template( - hass: HomeAssistant, setup_single_attribute_state_cover -) -> None: +@pytest.mark.usefixtures("setup_single_attribute_state_cover") +async def test_availability_template(hass: HomeAssistant) -> None: """Test availability template.""" hass.states.async_set("availability_state.state", STATE_OFF) + # This forces a trigger for trigger based entities + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_OFF) await hass.async_block_till_done() assert hass.states.get(TEST_ENTITY_ID).state == STATE_UNAVAILABLE hass.states.async_set("availability_state.state", STATE_ON) + # This forces a trigger for trigger based entities + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON) await hass.async_block_till_done() assert hass.states.get(TEST_ENTITY_ID).state != STATE_UNAVAILABLE @@ -1071,15 +1350,35 @@ async def test_availability_template( }, template.DOMAIN, ), + ( + { + "template": { + **TEST_STATE_TRIGGER, + "cover": { + **NAMED_COVER_ACTIONS, + "state": "{{ true }}", + "availability": "{{ x - 12 }}", + }, + } + }, + template.DOMAIN, + ), ], ) @pytest.mark.usefixtures("start_ha") async def test_invalid_availability_template_keeps_component_available( - hass: HomeAssistant, caplog_setup_text + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, caplog_setup_text ) -> None: """Test that an invalid availability keeps the device available.""" + + # This forces a trigger for trigger based entities + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON) + await hass.async_block_till_done() + assert hass.states.get(TEST_ENTITY_ID) != STATE_UNAVAILABLE - assert "UndefinedError: 'x' is undefined" in caplog_setup_text + + err = "UndefinedError: 'x' is undefined" + assert err in caplog_setup_text or err in caplog.text @pytest.mark.parametrize( @@ -1088,11 +1387,10 @@ async def test_invalid_availability_template_keeps_component_available( ) @pytest.mark.parametrize( "style", - [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN], + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) -async def test_device_class( - hass: HomeAssistant, setup_single_attribute_state_cover -) -> None: +@pytest.mark.usefixtures("setup_single_attribute_state_cover") +async def test_device_class(hass: HomeAssistant) -> None: """Test device class.""" state = hass.states.get(TEST_ENTITY_ID) assert state.attributes.get("device_class") == "door" @@ -1104,11 +1402,10 @@ async def test_device_class( ) @pytest.mark.parametrize( "style", - [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN], + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) -async def test_invalid_device_class( - hass: HomeAssistant, setup_single_attribute_state_cover -) -> None: +@pytest.mark.usefixtures("setup_single_attribute_state_cover") +async def test_invalid_device_class(hass: HomeAssistant) -> None: """Test device class.""" state = hass.states.get(TEST_ENTITY_ID) assert not state @@ -1138,9 +1435,23 @@ async def test_invalid_device_class( ], ConfigurationStyle.MODERN, ), + ( + [ + { + "name": "test_template_cover_01", + **UNIQUE_ID_CONFIG, + }, + { + "name": "test_template_cover_02", + **UNIQUE_ID_CONFIG, + }, + ], + ConfigurationStyle.TRIGGER, + ), ], ) -async def test_unique_id(hass: HomeAssistant, setup_cover) -> None: +@pytest.mark.usefixtures("setup_cover") +async def test_unique_id(hass: HomeAssistant) -> None: """Test unique_id option only creates one cover per id.""" assert len(hass.states.async_all()) == 1 @@ -1211,9 +1522,18 @@ async def test_nested_unique_id( "state": "{{ is_state('binary_sensor.garage_door_sensor', 'off') }}", }, ), + ( + ConfigurationStyle.TRIGGER, + { + "name": "Garage Door", + **COVER_ACTIONS, + "state": "{{ is_state('binary_sensor.garage_door_sensor', 'off') }}", + }, + ), ], ) -async def test_state_gets_lowercased(hass: HomeAssistant, setup_cover) -> None: +@pytest.mark.usefixtures("setup_cover") +async def test_state_gets_lowercased(hass: HomeAssistant) -> None: """Test True/False is lowercased.""" hass.states.async_set("binary_sensor.garage_door_sensor", "off") @@ -1242,12 +1562,12 @@ async def test_state_gets_lowercased(hass: HomeAssistant, setup_cover) -> None: [ (ConfigurationStyle.LEGACY, "icon_template"), (ConfigurationStyle.MODERN, "icon"), + (ConfigurationStyle.TRIGGER, "icon"), ], ) +@pytest.mark.usefixtures("setup_single_attribute_state_cover") async def test_self_referencing_icon_with_no_template_is_not_a_loop( - hass: HomeAssistant, - setup_single_attribute_state_cover, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test a self referencing icon with no value template is not a loop.""" assert len(hass.states.async_all()) == 1 @@ -1255,6 +1575,11 @@ async def test_self_referencing_icon_with_no_template_is_not_a_loop( assert "Template loop detected" not in caplog.text +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) @pytest.mark.parametrize( ("script", "supported_feature"), [ @@ -1269,32 +1594,11 @@ async def test_self_referencing_icon_with_no_template_is_not_a_loop( ), ], ) -async def test_emtpy_action_config( - hass: HomeAssistant, script: str, supported_feature: CoverEntityFeature +@pytest.mark.usefixtures("setup_empty_action") +async def test_empty_action_config( + hass: HomeAssistant, supported_feature: CoverEntityFeature ) -> None: """Test configuration with empty script.""" - with assert_setup_component(1, COVER_DOMAIN): - assert await async_setup_component( - hass, - COVER_DOMAIN, - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - "open_cover": [], - "close_cover": [], - script: [], - } - }, - } - }, - ) - - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() - state = hass.states.get("cover.test_template_cover") assert ( state.attributes["supported_features"]