1
0
mirror of https://github.com/home-assistant/core.git synced 2026-07-03 20:56:06 +01:00
Files
core/tests/components/template/test_event.py
T

696 lines
20 KiB
Python

"""The tests for the Template event platform."""
from typing import Any
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components import event, template
from homeassistant.components.template.const import CONF_ATTRIBUTES, CONF_PICTURE
from homeassistant.components.template.coordinator import TriggerUpdateCoordinator
from homeassistant.components.template.event import (
CONF_EVENT_TYPE,
CONF_EVENT_TYPES,
TriggerEventEntity,
)
from homeassistant.const import (
ATTR_ENTITY_PICTURE,
ATTR_FRIENDLY_NAME,
ATTR_ICON,
CONF_ICON,
CONF_NAME,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.template import Template
from .conftest import (
RESTORE_STATE_SAVED_ATTRIBUTES,
RESTORE_STATE_UPDATED_ATTRIBUTES,
ConfigurationStyle,
TemplatePlatformSetup,
assert_state_and_attributes,
async_get_flow_preview_state,
async_trigger,
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, MockEntityPlatform, mock_restore_cache
from tests.conftest import WebSocketGenerator
TEST_STATE_ENTITY_ID = "sensor.test_state"
TEST_ATTRIBUTE_ENTITY_ID = "sensor.test_attribute"
TEST_EVENT = TemplatePlatformSetup(
event.DOMAIN,
"template_event",
make_test_trigger(TEST_STATE_ENTITY_ID, TEST_ATTRIBUTE_ENTITY_ID),
)
TEST_EVENT_TYPES_TEMPLATE = "{{ ['single', 'double', 'hold'] }}"
TEST_EVENT_TYPE_TEMPLATE = "{{ 'single' }}"
TEST_EVENT_CONFIG = {
"event_types": TEST_EVENT_TYPES_TEMPLATE,
"event_type": TEST_EVENT_TYPE_TEMPLATE,
}
TEST_FROZEN_INPUT = "2024-07-09 00:00:00+00:00"
TEST_FROZEN_STATE = "2024-07-09T00:00:00.000+00:00"
@pytest.fixture
async def setup_base_event(
hass: HomeAssistant,
count: int,
style: ConfigurationStyle,
config: dict[str, Any],
) -> None:
"""Do setup of event integration."""
await setup_entity(hass, TEST_EVENT, style, count, config)
@pytest.fixture
async def setup_event(
hass: HomeAssistant,
count: int,
style: ConfigurationStyle,
event_type_template: str,
event_types_template: str,
extra_config: dict[str, Any] | None,
) -> None:
"""Do setup of event integration."""
await setup_entity(
hass,
TEST_EVENT,
style,
count,
{
"event_type": event_type_template,
"event_types": event_types_template,
},
extra_config=extra_config,
)
@pytest.fixture
async def setup_single_attribute_state_event(
hass: HomeAssistant,
count: int,
style: ConfigurationStyle,
event_type_template: str,
event_types_template: str,
attribute: str,
attribute_template: str,
) -> None:
"""Do setup of event integration testing a single attribute."""
await setup_entity(
hass,
TEST_EVENT,
style,
count,
{
"event_type": event_type_template,
"event_types": event_types_template,
},
extra_config={attribute: attribute_template}
if attribute and attribute_template
else {},
)
@pytest.mark.freeze_time(TEST_FROZEN_INPUT)
async def test_setup_config_entry(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
) -> None:
"""Test the config flow."""
await async_trigger(
hass,
TEST_STATE_ENTITY_ID,
"single",
{},
)
template_config_entry = MockConfigEntry(
data={},
domain=template.DOMAIN,
options={
"name": TEST_EVENT.object_id,
**TEST_EVENT_CONFIG,
"template_type": event.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(TEST_EVENT.entity_id)
assert state is not None
assert state == snapshot
@pytest.mark.freeze_time(TEST_FROZEN_INPUT)
async def test_device_id(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test for device for Template."""
device_config_entry = MockConfigEntry()
device_config_entry.add_to_hass(hass)
device_entry = device_registry.async_get_or_create(
config_entry_id=device_config_entry.entry_id,
identifiers={("test", "identifier_test")},
connections={("mac", "30:31:32:33:34:35")},
)
await hass.async_block_till_done()
assert device_entry is not None
assert device_entry.id is not None
template_config_entry = MockConfigEntry(
data={},
domain=template.DOMAIN,
options={
"name": "My template",
"event_type": TEST_EVENT_TYPE_TEMPLATE,
"event_types": TEST_EVENT_TYPES_TEMPLATE,
"template_type": "event",
"device_id": device_entry.id,
},
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()
template_entity = entity_registry.async_get("event.my_template")
assert template_entity is not None
assert template_entity.device_id == device_entry.id
@pytest.mark.parametrize(
("count", "event_types_template", "extra_config"),
[(1, TEST_EVENT_TYPES_TEMPLATE, None)],
)
@pytest.mark.parametrize(
("style", "expected_state"),
[
(ConfigurationStyle.MODERN, STATE_UNKNOWN),
(ConfigurationStyle.TRIGGER, STATE_UNKNOWN),
],
)
@pytest.mark.parametrize("event_type_template", ["{{states.test['big.fat...']}}"])
@pytest.mark.usefixtures("setup_event")
async def test_event_type_syntax_error(
hass: HomeAssistant,
expected_state: str,
) -> None:
"""Test template event_type with render error."""
state = hass.states.get(TEST_EVENT.entity_id)
assert state.state == expected_state
@pytest.mark.parametrize(
("count", "event_type_template", "event_types_template", "extra_config"),
[(1, "{{ states('sensor.test_state') }}", TEST_EVENT_TYPES_TEMPLATE, None)],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.parametrize(
("event", "expected"),
[
("single", "single"),
("double", "double"),
("hold", "hold"),
],
)
@pytest.mark.usefixtures("setup_event")
async def test_event_type_template(
hass: HomeAssistant,
event: str,
expected: str,
) -> None:
"""Test template event_type."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, event)
state = hass.states.get(TEST_EVENT.entity_id)
assert state.attributes["event_type"] == expected
@pytest.mark.parametrize(
("count", "event_type_template", "event_types_template", "extra_config"),
[(1, "{{ states('sensor.test_state') }}", TEST_EVENT_TYPES_TEMPLATE, None)],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_event")
@pytest.mark.freeze_time(TEST_FROZEN_INPUT)
async def test_event_type_template_updates(
hass: HomeAssistant,
) -> None:
"""Test template event_type updates."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, "single")
state = hass.states.get(TEST_EVENT.entity_id)
assert state.state == TEST_FROZEN_STATE
assert state.attributes["event_type"] == "single"
await async_trigger(hass, TEST_STATE_ENTITY_ID, "double")
state = hass.states.get(TEST_EVENT.entity_id)
assert state.state == TEST_FROZEN_STATE
assert state.attributes["event_type"] == "double"
await async_trigger(hass, TEST_STATE_ENTITY_ID, "hold")
state = hass.states.get(TEST_EVENT.entity_id)
assert state.state == TEST_FROZEN_STATE
assert state.attributes["event_type"] == "hold"
@pytest.mark.parametrize(
("count", "event_types_template", "extra_config"),
[(1, TEST_EVENT_TYPES_TEMPLATE, None)],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.parametrize(
"event_type_template",
[
"{{ None }}",
"{{ 7 }}",
"{{ 'unknown' }}",
"{{ 'tripple_double' }}",
],
)
@pytest.mark.usefixtures("setup_event")
async def test_event_type_invalid(
hass: HomeAssistant,
) -> None:
"""Test template event_type."""
state = hass.states.get(TEST_EVENT.entity_id)
assert state.state == STATE_UNKNOWN
assert state.attributes["event_type"] is None
@pytest.mark.parametrize(
("count", "event_type_template", "event_types_template"),
[(1, "{{ states('sensor.test_state') }}", TEST_EVENT_TYPES_TEMPLATE)],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.parametrize(
("attribute", "attribute_template", "key", "expected"),
[
(
"picture",
"{% if is_state('sensor.test_state', 'double') %}something{% endif %}",
ATTR_ENTITY_PICTURE,
"something",
),
(
"icon",
"{% if is_state('sensor.test_state', 'double') %}mdi:something{% endif %}",
ATTR_ICON,
"mdi:something",
),
],
)
@pytest.mark.usefixtures("setup_single_attribute_state_event")
async def test_entity_picture_and_icon_templates(
hass: HomeAssistant, key: str, expected: str
) -> None:
"""Test picture and icon template."""
state = await async_trigger(hass, TEST_STATE_ENTITY_ID, "single")
state = hass.states.get(TEST_EVENT.entity_id)
assert state.attributes.get(key) in ("", None)
state = await async_trigger(hass, TEST_STATE_ENTITY_ID, "double")
state = hass.states.get(TEST_EVENT.entity_id)
assert state.attributes[key] == expected
@pytest.mark.parametrize(
("count", "event_type_template", "extra_config"),
[(1, "{{ None }}", None)],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.parametrize(
("event_types_template", "expected"),
[
(
"{{ ['Strobe color', 'Police', 'Christmas', 'RGB', 'Random Loop'] }}",
["Strobe color", "Police", "Christmas", "RGB", "Random Loop"],
),
(
"{{ ['Police', 'RGB', 'Random Loop'] }}",
["Police", "RGB", "Random Loop"],
),
("{{ [] }}", []),
("{{ '[]' }}", []),
("{{ 124 }}", []),
("{{ '124' }}", []),
("{{ none }}", []),
("", []),
],
)
@pytest.mark.usefixtures("setup_event")
async def test_event_types_template(hass: HomeAssistant, expected: str) -> None:
"""Test template event_types."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
state = hass.states.get(TEST_EVENT.entity_id)
assert state.attributes["event_types"] == expected
@pytest.mark.parametrize(
("count", "event_type_template", "event_types_template", "extra_config"),
[
(
1,
"{{ states('sensor.test_state') }}",
"{{ state_attr('sensor.test_state', 'options') or ['unknown'] }}",
None,
)
],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_event")
@pytest.mark.freeze_time(TEST_FROZEN_INPUT)
async def test_event_types_template_updates(hass: HomeAssistant) -> None:
"""Test template event_type update with entity."""
await async_trigger(
hass, TEST_STATE_ENTITY_ID, "single", {"options": ["single", "double", "hold"]}
)
await hass.async_block_till_done()
state = hass.states.get(TEST_EVENT.entity_id)
assert state.state == TEST_FROZEN_STATE
assert state.attributes["event_type"] == "single"
assert state.attributes["event_types"] == ["single", "double", "hold"]
await async_trigger(
hass, TEST_STATE_ENTITY_ID, "double", {"options": ["double", "hold"]}
)
await hass.async_block_till_done()
state = hass.states.get(TEST_EVENT.entity_id)
assert state.state == TEST_FROZEN_STATE
assert state.attributes["event_type"] == "double"
assert state.attributes["event_types"] == ["double", "hold"]
@pytest.mark.parametrize(
(
"count",
"event_type_template",
"event_types_template",
"attribute",
"attribute_template",
),
[
(
1,
"{{ states('sensor.test_state') }}",
TEST_EVENT_TYPES_TEMPLATE,
"availability",
"{{ states('sensor.test_state') in ['single', 'double', 'hold'] }}",
)
],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_single_attribute_state_event")
async def test_available_template_with_entities(hass: HomeAssistant) -> None:
"""Test availability templates with values from other entities."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, "single")
state = hass.states.get(TEST_EVENT.entity_id)
assert state.state != STATE_UNAVAILABLE
assert state.attributes["event_type"] == "single"
await async_trigger(hass, TEST_STATE_ENTITY_ID, "triple")
state = hass.states.get(TEST_EVENT.entity_id)
assert state.state == STATE_UNAVAILABLE
assert "event_type" not in state.attributes
@pytest.mark.parametrize(
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
)
@pytest.mark.parametrize(
"config",
[
{
"event_type": "{{ states('sensor.test_state') }}",
"event_types": TEST_EVENT_TYPES_TEMPLATE,
"attributes": {
"plus_one": "{{ states('sensor.test_attribute') | int(0) + 1 }}",
"plus_two": "{{ states('sensor.test_attribute') | int(0) + 2 }}",
},
},
],
)
async def test_restore_state(
hass: HomeAssistant,
style: ConfigurationStyle,
config: dict,
) -> None:
"""Test restoring template event entities."""
# Ensure the initial state is None so that restore data is honored
await async_trigger(hass, TEST_STATE_ENTITY_ID, None)
restored_attributes = {
"plus_one": 55,
}
setup_mock_template_entity_restore_state(
hass,
TEST_EVENT,
"2021-01-01T23:59:59.123+00:00",
saved_extra_data={
"last_event_type": "hold",
"last_event_attributes": restored_attributes,
},
saved_attributes=restored_attributes,
)
await setup_restore_template_entity(
hass, TEST_EVENT, style, config, "is_state('sensor.test_attribute', '2')"
)
test_state = "2021-01-01T23:59:59.123+00:00"
state = assert_state_and_attributes(
hass,
TEST_EVENT,
test_state,
{**restored_attributes, **RESTORE_STATE_SAVED_ATTRIBUTES},
)
assert "plus_two" not in state.attributes
await async_trigger(hass, TEST_STATE_ENTITY_ID, "double")
await async_trigger(hass, TEST_ATTRIBUTE_ENTITY_ID, 2)
state = assert_state_and_attributes(
hass,
TEST_EVENT,
expected_attributes={
"plus_one": 3,
"plus_two": 4,
"event_type": "double",
"event_types": ["single", "double", "hold"],
**RESTORE_STATE_UPDATED_ATTRIBUTES,
},
)
assert state.state != test_state
def _make_trigger_event_entity(hass: HomeAssistant) -> TriggerEventEntity:
"""Create a trigger event entity that renders fresh, static attributes."""
config = {
CONF_NAME: Template("fresh_name", hass),
CONF_ICON: Template("mdi:fresh", hass),
CONF_PICTURE: Template("/local/fresh.png", hass),
CONF_EVENT_TYPE: Template("fresh", hass),
CONF_EVENT_TYPES: Template("{{ ['fresh'] }}", hass),
CONF_ATTRIBUTES: {"attr": Template("fresh_attr", hass)},
}
coordinator = TriggerUpdateCoordinator(hass, {})
entity = TriggerEventEntity(hass, coordinator, config)
entity.entity_id = "event.test"
entity.platform = MockEntityPlatform(hass)
return entity
def _mock_stale_restore_cache(hass: HomeAssistant) -> None:
"""Prime the restore cache with stale attributes for the test entity."""
mock_restore_cache(
hass,
(
State(
"event.test",
"2021-01-01T00:00:00+00:00",
{
ATTR_FRIENDLY_NAME: "stale_name",
ATTR_ICON: "mdi:stale",
ATTR_ENTITY_PICTURE: "/local/stale.png",
"attr": "stale_attr",
},
),
),
)
async def test_trigger_restore_does_not_clobber_rendered_attributes(
hass: HomeAssistant,
) -> None:
"""A trigger that already fired must win over restored state.
Regression test for the race where the trigger fires (populating the
coordinator) before the entity is added to hass. ``_process_data`` renders
fresh attributes which restore would otherwise overwrite with stale values.
"""
entity = _make_trigger_event_entity(hass)
# Simulate the trigger having already fired before the entity is added.
entity.coordinator._execute_update({})
assert entity.coordinator.data is not None
_mock_stale_restore_cache(hass)
await entity.async_added_to_hass()
await hass.async_block_till_done()
# The freshly rendered values must win over the stale restored ones.
assert entity.name == "fresh_name"
assert entity.icon == "mdi:fresh"
assert entity.entity_picture == "/local/fresh.png"
assert entity.extra_state_attributes == {"attr": "fresh_attr"}
async def test_trigger_restores_when_not_yet_triggered(
hass: HomeAssistant,
) -> None:
"""When the trigger has not fired, restored attributes are applied."""
entity = _make_trigger_event_entity(hass)
assert entity.coordinator.data is None
_mock_stale_restore_cache(hass)
await entity.async_added_to_hass()
await hass.async_block_till_done()
# No fresh data yet, so the restored values are surfaced.
assert entity.name == "stale_name"
assert entity.icon == "mdi:stale"
assert entity.entity_picture == "/local/stale.png"
assert entity.extra_state_attributes == {"attr": "stale_attr"}
@pytest.mark.parametrize(
(
"count",
"event_type_template",
"event_types_template",
"attribute",
"attribute_template",
),
[
(
1,
TEST_EVENT_TYPE_TEMPLATE,
TEST_EVENT_TYPES_TEMPLATE,
"availability",
"{{ x - 12 }}",
)
],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_single_attribute_state_event")
async def test_invalid_availability_template_keeps_component_available(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
caplog_setup_text,
) -> None:
"""Test that an invalid availability keeps the device available."""
await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
assert hass.states.get(TEST_EVENT.entity_id).state != STATE_UNAVAILABLE
error = "UndefinedError: 'x' is undefined"
assert error in caplog_setup_text or error in caplog.text
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
async def test_unique_id(hass: HomeAssistant, style: ConfigurationStyle) -> None:
"""Test unique_id option only creates one event per id."""
await setup_and_test_unique_id(hass, TEST_EVENT, style, TEST_EVENT_CONFIG)
@pytest.mark.parametrize(
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
)
async def test_nested_unique_id(
hass: HomeAssistant,
style: ConfigurationStyle,
entity_registry: er.EntityRegistry,
) -> None:
"""Test a template unique_id propagates to event unique_ids."""
await setup_and_test_nested_unique_id(
hass, TEST_EVENT, style, entity_registry, TEST_EVENT_CONFIG
)
@pytest.mark.freeze_time(TEST_FROZEN_INPUT)
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,
event.DOMAIN,
{"name": "My template", **TEST_EVENT_CONFIG},
)
assert state["state"] == TEST_FROZEN_STATE
assert state["attributes"]["event_type"] == "single"
assert state["attributes"]["event_types"] == ["single", "double", "hold"]