Files

727 lines
21 KiB
Python

"""template conftest."""
from dataclasses import dataclass
from enum import Enum, StrEnum
from itertools import chain
from unittest.mock import AsyncMock, Mock
import pytest
import voluptuous as vol
from homeassistant.components import template
from homeassistant.components.device_automation import toggle_entity
from homeassistant.config_entries import SOURCE_USER
from homeassistant.core import HomeAssistant, ServiceCall, State
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.typing import ConfigType
from homeassistant.setup import async_setup_component
from tests.common import (
MockConfigEntry,
assert_setup_component,
async_mock_service,
mock_platform,
mock_restore_cache,
mock_restore_cache_with_extra_data,
)
from tests.conftest import WebSocketGenerator
_TEST_EXTRA_ATTRIBUTES_ENTITY_ID = "sensor.test_extra_attributes"
class ConfigurationStyle(Enum):
"""Configuration Styles for template testing."""
MODERN = "Modern"
TRIGGER = "Trigger"
class Brewery(StrEnum):
"""Test enum."""
MMMM = "mmmm"
BEER = "beer"
IS = "is"
GOOD = "good"
def make_test_trigger(*entities: str) -> dict:
"""Make a test state trigger."""
return {
"trigger": [
{
"trigger": "state",
"entity_id": list(chain(entities, (_TEST_EXTRA_ATTRIBUTES_ENTITY_ID,))),
},
{"platform": "event", "event_type": "test_event"},
],
"variables": {"triggering_entity": "{{ trigger.entity_id }}"},
"action": [
{"event": "action_event", "event_data": {"what": "{{ triggering_entity }}"}}
],
}
def make_test_action(action: str, extra_data: ConfigType | None = None) -> ConfigType:
"""Make a test action."""
data = extra_data or {}
return {
action: {
"action": "test.automation",
"data": {"caller": "{{ this.entity_id }}", "action": action, **data},
}
}
def make_mock_device_actions(
actions: list[str],
platform_setup: TemplatePlatformSetup,
device_entry: dr.DeviceEntry,
entity_entry: er.RegistryEntry,
) -> ConfigType:
"""Make actions for device testing."""
return {
action: [
{
"action": "test.automation",
"data": {
"action": "fake_action",
"caller": platform_setup.entity_id,
},
},
{
"domain": "fake_integration",
"type": "turn_on",
"device_id": device_entry.id,
"entity_id": entity_entry.id,
"metadata": {"secondary": False},
},
]
for action in actions
}
async def setup_mock_devices(
hass: HomeAssistant,
domain: str,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> tuple[TemplatePlatformSetup, dr.DeviceEntry, er.RegistryEntry]:
"""Setup mock devices for testing."""
FAKE_DOMAIN = "fake_integration"
hass.config.components.add(FAKE_DOMAIN)
async def _async_get_actions(
hass: HomeAssistant, device_id: str
) -> list[dict[str, str]]:
"""List device actions."""
return await toggle_entity.async_get_actions(hass, device_id, FAKE_DOMAIN)
mock_platform(
hass,
f"{FAKE_DOMAIN}.device_action",
Mock(
ACTION_SCHEMA=toggle_entity.ACTION_SCHEMA.extend(
{vol.Required("domain"): FAKE_DOMAIN}
),
async_get_actions=_async_get_actions,
async_call_action_from_config=AsyncMock(),
spec=[
"ACTION_SCHEMA",
"async_get_actions",
"async_call_action_from_config",
],
),
)
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
entity_entry = entity_registry.async_get_or_create(
"fake_integration", "test", "5678", device_id=device_entry.id
)
await hass.async_block_till_done()
platform_setup = TemplatePlatformSetup(
domain, "test_entity", make_test_trigger("sensor.trigger")
)
return (platform_setup, device_entry, entity_entry)
def assert_action(
platform_setup: TemplatePlatformSetup,
calls: list[ServiceCall],
expected_calls: int,
expected_action: str,
index: int = -1,
**kwargs,
) -> None:
"""Validate the action was properly called."""
assert len(calls) == expected_calls
assert calls[index].data["action"] == expected_action
assert calls[index].data["caller"] == platform_setup.entity_id
for key, value in kwargs.items():
assert calls[index].data[key] == value
async def async_trigger(
hass: HomeAssistant,
entity_id: str,
state: str | None = None,
attributes: dict | None = None,
) -> None:
"""Trigger a state change."""
hass.states.async_set(entity_id, state, attributes)
await hass.async_block_till_done()
async def async_setup_modern_state_format(
hass: HomeAssistant,
domain: str,
count: int,
config: ConfigType | list[ConfigType],
extra_section_config: ConfigType | None = None,
) -> None:
"""Do setup of template integration via modern format."""
with assert_setup_component(count, template.DOMAIN):
assert await async_setup_component(
hass,
template.DOMAIN,
{"template": {domain: config, **(extra_section_config or {})}},
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
async def async_setup_modern_trigger_format(
hass: HomeAssistant,
domain: str,
trigger: dict,
count: int,
config: ConfigType | list[ConfigType],
extra_section_config: ConfigType | None = None,
) -> None:
"""Do setup of template integration via trigger format."""
config = {"template": {domain: config, **trigger, **(extra_section_config or {})}}
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()
@dataclass(frozen=True)
class TemplatePlatformSetup:
"""Template Platform Setup Information."""
domain: str
object_id: str
trigger: ConfigType
@property
def entity_id(self) -> str:
"""Return test entity ID."""
return f"{self.domain}.{self.object_id}"
async def setup_entity(
hass: HomeAssistant,
platform_setup: TemplatePlatformSetup,
style: ConfigurationStyle,
count: int,
config: ConfigType,
state_template: str | None = None,
extra_config: ConfigType | None = None,
attributes: ConfigType | None = None,
extra_section_config: ConfigType | None = None,
) -> None:
"""Do setup of a template entity based on the configuration style."""
entity_config = {
"name": platform_setup.object_id,
**({"state": state_template} if state_template else {}),
**config,
**({"attributes": attributes} if attributes else {}),
**(extra_config or {}),
}
if style is ConfigurationStyle.MODERN:
await async_setup_modern_state_format(
hass, platform_setup.domain, count, entity_config, extra_section_config
)
elif style is ConfigurationStyle.TRIGGER:
await async_setup_modern_trigger_format(
hass,
platform_setup.domain,
platform_setup.trigger,
count,
entity_config,
extra_section_config,
)
async def setup_and_test_unique_id(
hass: HomeAssistant,
platform_setup: TemplatePlatformSetup,
style: ConfigurationStyle,
entity_config: ConfigType | None,
state_template: str | None = None,
) -> None:
"""Setup 2 entities with the same unique_id and verify only 1 entity is created.
The entity_config not provide name or unique_id, those are added automatically.
"""
state_config = {"state": state_template} if state_template else {}
entity_config = {
"unique_id": "not-so_-unique-anymore",
**(entity_config or {}),
**state_config,
}
entities = [
{"name": "template_entity_1", **entity_config},
{"name": "template_entity_2", **entity_config},
]
if style is ConfigurationStyle.MODERN:
await async_setup_modern_state_format(hass, platform_setup.domain, 1, entities)
elif style is ConfigurationStyle.TRIGGER:
await async_setup_modern_trigger_format(
hass, platform_setup.domain, platform_setup.trigger, 1, entities
)
assert len(hass.states.async_all(platform_setup.domain)) == 1
async def setup_and_test_nested_unique_id(
hass: HomeAssistant,
platform_setup: TemplatePlatformSetup,
style: ConfigurationStyle,
entity_registry: er.EntityRegistry,
entity_config: ConfigType | None,
state_template: str | None = None,
) -> None:
"""Setup 2 entities with unique_ids in a template section with a unique_id.
The test will verify that 2 entities are created where the unique_id
appends the section unique_id to each entity unique_id.
The entity_config should not provide name or unique_id, those are
added automatically.
"""
state_config = {"state": state_template} if state_template else {}
entities = [
{"name": "test_a", "unique_id": "a", **(entity_config or {}), **state_config},
{"name": "test_b", "unique_id": "b", **(entity_config or {}), **state_config},
]
extra_section_config = {"unique_id": "x"}
if style is ConfigurationStyle.MODERN:
await async_setup_modern_state_format(
hass, platform_setup.domain, 1, entities, extra_section_config
)
elif style is ConfigurationStyle.TRIGGER:
await async_setup_modern_trigger_format(
hass,
platform_setup.domain,
platform_setup.trigger,
1,
entities,
extra_section_config,
)
assert len(hass.states.async_all(platform_setup.domain)) == 2
entry = entity_registry.async_get(f"{platform_setup.domain}.test_a")
assert entry.unique_id == "x-a"
entry = entity_registry.async_get(f"{platform_setup.domain}.test_b")
assert entry.unique_id == "x-b"
@pytest.fixture
def calls(hass: HomeAssistant) -> list[ServiceCall]:
"""Track calls to a mock service."""
return async_mock_service(hass, "test", "automation")
@pytest.fixture
async def start_ha(
hass: HomeAssistant, count: int, domain: str, config: ConfigType
) -> None:
"""Do setup of integration."""
with assert_setup_component(count, domain):
assert await async_setup_component(
hass,
domain,
config,
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
@pytest.fixture
async def caplog_setup_text(caplog: pytest.LogCaptureFixture) -> str:
"""Return setup log of integration."""
return caplog.text
def _create_bad_action_config(action: str, config: ConfigType) -> ConfigType:
"""Create a bad device action."""
return {
action: {
"type": "turn_off",
"device_id": "70c5f67ec2f82f9ba128fe6e99eb7dfa",
"entity_id": "c7e6f3753cb18937f2147bbbdccdd949",
"domain": "light",
},
**config,
}
async def assert_invalid_yaml_actions_do_not_create_entities(
hass: HomeAssistant,
platform_setup: TemplatePlatformSetup,
style: ConfigurationStyle,
config: ConfigType,
action: str,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Assert invalid yaml actions on entity services do not create entities."""
await setup_entity(
hass,
platform_setup,
style,
1,
{
"default_entity_id": platform_setup.entity_id,
**_create_bad_action_config(action, config),
},
)
assert len(hass.states.async_all(platform_setup.domain)) == 0
error = f"The '{action}' actions for {platform_setup.object_id} failed to setup: Unknown device '70c5f67ec2f82f9ba128fe6e99eb7dfa'"
assert error in caplog.text
async def assert_invalid_config_entry_actions_do_not_create_entities(
hass: HomeAssistant,
platform_setup: TemplatePlatformSetup,
config: ConfigType,
action: str,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Assert invalid config entry actions on entity services do not create entities."""
template_config_entry = MockConfigEntry(
data={},
domain=template.DOMAIN,
options={
"name": platform_setup.object_id,
"template_type": platform_setup.domain,
**_create_bad_action_config(action, config),
},
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()
assert len(hass.states.async_all(platform_setup.domain)) == 0
error = f"The '{action}' actions for {platform_setup.object_id} failed to setup: Unknown device '70c5f67ec2f82f9ba128fe6e99eb7dfa'"
assert error in caplog.text
async def async_get_flow_preview_state(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
domain: str,
user_input: ConfigType,
) -> ConfigType:
"""Test the config flow preview."""
client = await hass_ws_client(hass)
result = await hass.config_entries.flow.async_init(
template.DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.MENU
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"next_step_id": domain},
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == domain
assert result["errors"] is None
assert result["preview"] == "template"
await client.send_json_auto_id(
{
"type": "template/start_preview",
"flow_id": result["flow_id"],
"flow_type": "config_flow",
"user_input": user_input,
}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] is None
msg = await client.receive_json()
return msg["event"]
def assert_state_and_attributes(
hass: HomeAssistant,
platform_setup: TemplatePlatformSetup,
expected_state: str | None = None,
expected_attributes: ConfigType | None = None,
) -> State:
"""Assert expected state and attributes."""
state = hass.states.get(platform_setup.entity_id)
assert state is not None
assert state.state == expected_state or expected_state is None
expected_attributes = expected_attributes or {}
for attribute, value in expected_attributes.items():
assert state.attributes.get(attribute) == value
return state
RESTORE_STATE_SAVED_ATTRIBUTES = {
"friendly_name": "Restored Name",
"icon": "mdi:restored",
"entity_picture": "local/restored.png",
}
RESTORE_STATE_UPDATED_ATTRIBUTES = {
"friendly_name": "Updated Name",
"icon": "mdi:updated",
"entity_picture": "local/updated.png",
}
def make_restore_state_built_in_attribute_templates(jinja_test: str) -> dict:
"""Make built in attribute templates for restore state testing."""
return {
"name": f"{{% if {jinja_test} %}}Updated Name{{% endif %}}",
"picture": f"{{% if {jinja_test} %}}local/updated.png{{% endif %}}",
"icon": f"{{% if {jinja_test} %}}mdi:updated{{% endif %}}",
}
def setup_mock_template_entity_restore_state(
hass: HomeAssistant,
platform_setup: TemplatePlatformSetup,
saved_state: str,
saved_extra_data: ConfigType | None = None,
saved_attributes: ConfigType | None = None,
) -> None:
"""Setup an entity and verify state is restored."""
saved_attributes = {
**RESTORE_STATE_SAVED_ATTRIBUTES,
**(saved_attributes or {}),
}
fake_state = State(
platform_setup.entity_id,
saved_state,
saved_attributes,
)
if saved_extra_data is not None:
mock_restore_cache_with_extra_data(hass, ((fake_state, saved_extra_data),))
else:
mock_restore_cache(hass, (fake_state,))
async def setup_restore_template_entity(
hass: HomeAssistant,
platform_setup: TemplatePlatformSetup,
style: ConfigurationStyle,
config: ConfigType,
jinja_test: str = "is_state('sensor.test_restore', 'on')",
) -> None:
"""Test that state and attributes are restored from the last state."""
default_entity_id = platform_setup.entity_id
await setup_entity(
hass,
platform_setup,
style,
1,
config={
"default_entity_id": default_entity_id,
**make_restore_state_built_in_attribute_templates(jinja_test),
**config,
},
)
async def assert_extra_template_attributes(
hass: HomeAssistant,
platform_setup: TemplatePlatformSetup,
style: ConfigurationStyle,
config: ConfigType,
) -> None:
"""Test extra template attributes and attribute order."""
# Trigger attributes are resolved in order, Modern are not.
setup_attributes = {
ConfigurationStyle.MODERN: {},
ConfigurationStyle.TRIGGER: {
"base": "{{ state_attr('sensor.test_extra_attributes', 'base') or 0 }}",
"plus_one": "{{ base + 1 }}",
},
}
await setup_entity(
hass,
platform_setup,
style,
1,
{
**config,
"attributes": {
"static": "{{ 'static' }}",
"dynamic": "It {{ state_attr('sensor.test_extra_attributes', 'dynamic') }}.",
**setup_attributes[style],
},
},
)
await async_trigger(
hass,
_TEST_EXTRA_ATTRIBUTES_ENTITY_ID,
"anything",
{
"dynamic": "",
"base": 1,
},
)
state = hass.states.get(platform_setup.entity_id)
assert state.attributes["static"] == "static"
assert state.attributes["dynamic"] == "It ."
# Assert attribute order for trigger entities
for attr, value in (
("base", 1),
("plus_one", 2),
):
assert (
attr not in state.attributes and style == ConfigurationStyle.MODERN
) or state.attributes[attr] == value
await async_trigger(
hass,
_TEST_EXTRA_ATTRIBUTES_ENTITY_ID,
"anything",
{
"dynamic": "works",
"base": 2,
},
)
state = hass.states.get(platform_setup.entity_id)
assert state.attributes["static"] == "static"
assert state.attributes["dynamic"] == "It works."
for attr, value in (
("base", 2),
("plus_one", 3),
):
assert (
attr not in state.attributes and style == ConfigurationStyle.MODERN
) or state.attributes[attr] == value
async def assert_attributes_template(
hass: HomeAssistant,
platform_setup: TemplatePlatformSetup,
style: ConfigurationStyle,
config: ConfigType,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test extra attributes template."""
await setup_entity(
hass,
platform_setup,
style,
1,
{
**config,
"attributes": "{{ state_attr('sensor.test_extra_attributes', 'attributes') or {} }}",
},
)
await async_trigger(
hass,
_TEST_EXTRA_ATTRIBUTES_ENTITY_ID,
"anything",
{
"attributes": {"beer": "empty", "happiness": "low"},
},
)
state = hass.states.get(platform_setup.entity_id)
assert state.attributes["beer"] == "empty"
assert "level" not in state.attributes
assert state.attributes["happiness"] == "low"
await async_trigger(
hass,
_TEST_EXTRA_ATTRIBUTES_ENTITY_ID,
"anything",
{
"attributes": {"beer": "full", "level": 100, "happiness": "high"},
},
)
state = hass.states.get(platform_setup.entity_id)
assert state.attributes["beer"] == "full"
assert state.attributes["level"] == 100
assert state.attributes["happiness"] == "high"
await async_trigger(
hass,
_TEST_EXTRA_ATTRIBUTES_ENTITY_ID,
"anything",
{
"attributes": {"run": True},
},
)
state = hass.states.get(platform_setup.entity_id)
assert state.attributes["run"] is True
assert "beer" not in state.attributes
assert "level" not in state.attributes
assert "happiness" not in state.attributes
error = "Error validating template result"
assert error not in caplog.text
await async_trigger(
hass,
_TEST_EXTRA_ATTRIBUTES_ENTITY_ID,
"anything",
{
"attributes": ["not a dict"],
},
)
assert error in caplog.text
state = hass.states.get(platform_setup.entity_id)
assert "run" not in state.attributes