mirror of
https://github.com/home-assistant/core.git
synced 2026-04-02 08:26:41 +01:00
Correct validation of scripts in template entities (#165226)
This commit is contained in:
@@ -80,6 +80,16 @@ LEGACY_FIELDS = {
|
||||
CONF_VALUE_TEMPLATE: CONF_STATE,
|
||||
}
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_ARM_AWAY_ACTION,
|
||||
CONF_ARM_CUSTOM_BYPASS_ACTION,
|
||||
CONF_ARM_HOME_ACTION,
|
||||
CONF_ARM_NIGHT_ACTION,
|
||||
CONF_ARM_VACATION_ACTION,
|
||||
CONF_DISARM_ACTION,
|
||||
CONF_TRIGGER_ACTION,
|
||||
)
|
||||
|
||||
DEFAULT_NAME = "Template Alarm Control Panel"
|
||||
|
||||
ALARM_CONTROL_PANEL_COMMON_SCHEMA = vol.Schema(
|
||||
@@ -152,6 +162,7 @@ async def async_setup_entry(
|
||||
StateAlarmControlPanelEntity,
|
||||
ALARM_CONTROL_PANEL_CONFIG_ENTRY_SCHEMA,
|
||||
True,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -172,6 +183,7 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_ALARM_CONTROL_PANELS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_NAME = "Template Button"
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
|
||||
SCRIPT_FIELDS = (CONF_PRESS,)
|
||||
|
||||
BUTTON_YAML_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PRESS): cv.SCRIPT_SCHEMA,
|
||||
@@ -66,6 +68,7 @@ async def async_setup_platform(
|
||||
None,
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -81,6 +84,7 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateButtonEntity,
|
||||
BUTTON_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -71,6 +71,14 @@ CONF_TILT_OPTIMISTIC = "tilt_optimistic"
|
||||
|
||||
CONF_OPEN_AND_CLOSE = "open_or_close"
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CLOSE_ACTION,
|
||||
OPEN_ACTION,
|
||||
POSITION_ACTION,
|
||||
STOP_ACTION,
|
||||
TILT_ACTION,
|
||||
)
|
||||
|
||||
TILT_FEATURES = (
|
||||
CoverEntityFeature.OPEN_TILT
|
||||
| CoverEntityFeature.CLOSE_TILT
|
||||
@@ -165,6 +173,7 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_COVERS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -181,6 +190,7 @@ async def async_setup_entry(
|
||||
StateCoverEntity,
|
||||
COVER_CONFIG_ENTRY_SCHEMA,
|
||||
True,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -87,6 +87,15 @@ LEGACY_FIELDS = {
|
||||
|
||||
DEFAULT_NAME = "Template Fan"
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_OFF_ACTION,
|
||||
CONF_ON_ACTION,
|
||||
CONF_SET_DIRECTION_ACTION,
|
||||
CONF_SET_OSCILLATING_ACTION,
|
||||
CONF_SET_PERCENTAGE_ACTION,
|
||||
CONF_SET_PRESET_MODE_ACTION,
|
||||
)
|
||||
|
||||
FAN_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_DIRECTION): cv.template,
|
||||
@@ -159,6 +168,7 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_FANS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -174,6 +184,7 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateFanEntity,
|
||||
FAN_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant.components import blueprint
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -25,7 +26,7 @@ from homeassistant.const import (
|
||||
SERVICE_RELOAD,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
|
||||
from homeassistant.helpers import issue_registry as ir, template
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
@@ -34,6 +35,7 @@ from homeassistant.helpers.entity_platform import (
|
||||
async_get_platforms,
|
||||
)
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||
from homeassistant.helpers.script import async_validate_actions_config
|
||||
from homeassistant.helpers.script_variables import ScriptVariables
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
@@ -208,6 +210,21 @@ def _format_template(value: Any, field: str | None = None) -> Any:
|
||||
return str(value)
|
||||
|
||||
|
||||
def _get_config_breadcrumbs(config: ConfigType) -> str:
|
||||
"""Try to coerce entity information from the config."""
|
||||
breadcrumb = "Template Entity"
|
||||
# Default entity id should be in most legacy configuration because
|
||||
# it's created from the legacy slug. Vacuum and Lock do not have a
|
||||
# slug, therefore we need to use the name or unique_id.
|
||||
if (default_entity_id := config.get(CONF_DEFAULT_ENTITY_ID)) is not None:
|
||||
breadcrumb = default_entity_id.split(".")[-1]
|
||||
elif (unique_id := config.get(CONF_UNIQUE_ID)) is not None:
|
||||
breadcrumb = f"unique_id: {unique_id}"
|
||||
elif (name := config.get(CONF_NAME)) and isinstance(name, template.Template):
|
||||
breadcrumb = name.template
|
||||
return breadcrumb
|
||||
|
||||
|
||||
def format_migration_config(
|
||||
config: ConfigType | list[ConfigType], depth: int = 0
|
||||
) -> ConfigType | list[ConfigType]:
|
||||
@@ -252,16 +269,7 @@ def create_legacy_template_issue(
|
||||
if domain not in PLATFORMS:
|
||||
return
|
||||
|
||||
breadcrumb = "Template Entity"
|
||||
# Default entity id should be in most legacy configuration because
|
||||
# it's created from the legacy slug. Vacuum and Lock do not have a
|
||||
# slug, therefore we need to use the name or unique_id.
|
||||
if (default_entity_id := config.get(CONF_DEFAULT_ENTITY_ID)) is not None:
|
||||
breadcrumb = default_entity_id.split(".")[-1]
|
||||
elif (unique_id := config.get(CONF_UNIQUE_ID)) is not None:
|
||||
breadcrumb = f"unique_id: {unique_id}"
|
||||
elif (name := config.get(CONF_NAME)) and isinstance(name, template.Template):
|
||||
breadcrumb = name.template
|
||||
breadcrumb = _get_config_breadcrumbs(config)
|
||||
|
||||
issue_id = f"{LEGACY_TEMPLATE_DEPRECATION_KEY}_{domain}_{breadcrumb}_{hashlib.md5(','.join(config.keys()).encode()).hexdigest()}"
|
||||
|
||||
@@ -296,6 +304,39 @@ def create_legacy_template_issue(
|
||||
)
|
||||
|
||||
|
||||
async def validate_template_scripts(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
script_options: tuple[str, ...] | None = None,
|
||||
) -> None:
|
||||
"""Validate template scripts."""
|
||||
if not script_options:
|
||||
return
|
||||
|
||||
def _humanize(err: Exception, data: Any) -> str:
|
||||
"""Humanize vol.Invalid, stringify other exceptions."""
|
||||
if isinstance(err, vol.Invalid):
|
||||
return humanize_error(data, err)
|
||||
return str(err)
|
||||
|
||||
breadcrumb: str | None = None
|
||||
for script_option in script_options:
|
||||
if (script_config := config.pop(script_option, None)) is not None:
|
||||
try:
|
||||
config[script_option] = await async_validate_actions_config(
|
||||
hass, script_config
|
||||
)
|
||||
except (vol.Invalid, HomeAssistantError) as err:
|
||||
if not breadcrumb:
|
||||
breadcrumb = _get_config_breadcrumbs(config)
|
||||
_LOGGER.error(
|
||||
"The '%s' actions for %s failed to setup: %s",
|
||||
script_option,
|
||||
breadcrumb,
|
||||
_humanize(err, script_config),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_template_platform(
|
||||
hass: HomeAssistant,
|
||||
domain: str,
|
||||
@@ -306,6 +347,7 @@ async def async_setup_template_platform(
|
||||
discovery_info: DiscoveryInfoType | None,
|
||||
legacy_fields: dict[str, str] | None = None,
|
||||
legacy_key: str | None = None,
|
||||
script_options: tuple[str, ...] | None = None,
|
||||
) -> None:
|
||||
"""Set up the Template platform."""
|
||||
if discovery_info is None:
|
||||
@@ -337,10 +379,14 @@ async def async_setup_template_platform(
|
||||
# Trigger Configuration
|
||||
if "coordinator" in discovery_info:
|
||||
if trigger_entity_cls:
|
||||
entities = [
|
||||
trigger_entity_cls(hass, discovery_info["coordinator"], config)
|
||||
for config in discovery_info["entities"]
|
||||
]
|
||||
entities = []
|
||||
for entity_config in discovery_info["entities"]:
|
||||
await validate_template_scripts(hass, entity_config, script_options)
|
||||
entities.append(
|
||||
trigger_entity_cls(
|
||||
hass, discovery_info["coordinator"], entity_config
|
||||
)
|
||||
)
|
||||
async_add_entities(entities)
|
||||
else:
|
||||
raise PlatformNotReady(
|
||||
@@ -349,6 +395,9 @@ async def async_setup_template_platform(
|
||||
return
|
||||
|
||||
# Modern Configuration
|
||||
for entity_config in discovery_info["entities"]:
|
||||
await validate_template_scripts(hass, entity_config, script_options)
|
||||
|
||||
async_create_template_tracking_entities(
|
||||
state_entity_cls,
|
||||
async_add_entities,
|
||||
@@ -365,6 +414,7 @@ async def async_setup_template_entry(
|
||||
state_entity_cls: type[TemplateEntity],
|
||||
config_schema: vol.Schema | vol.All,
|
||||
replace_value_template: bool = False,
|
||||
script_options: tuple[str, ...] | None = None,
|
||||
) -> None:
|
||||
"""Setup the Template from a config entry."""
|
||||
options = dict(config_entry.options)
|
||||
@@ -377,6 +427,7 @@ async def async_setup_template_entry(
|
||||
options[CONF_STATE] = options.pop(CONF_VALUE_TEMPLATE)
|
||||
|
||||
validated_config = config_schema(options)
|
||||
await validate_template_scripts(hass, validated_config, script_options)
|
||||
|
||||
async_add_entities(
|
||||
[state_entity_cls(hass, validated_config, config_entry.entry_id)]
|
||||
|
||||
@@ -129,6 +129,18 @@ LEGACY_FIELDS = {
|
||||
|
||||
DEFAULT_NAME = "Template Light"
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_EFFECT_ACTION,
|
||||
CONF_HS_ACTION,
|
||||
CONF_LEVEL_ACTION,
|
||||
CONF_OFF_ACTION,
|
||||
CONF_ON_ACTION,
|
||||
CONF_RGB_ACTION,
|
||||
CONF_RGBW_ACTION,
|
||||
CONF_RGBWW_ACTION,
|
||||
CONF_TEMPERATURE_ACTION,
|
||||
)
|
||||
|
||||
LIGHT_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Inclusive(CONF_EFFECT_ACTION, "effect"): cv.SCRIPT_SCHEMA,
|
||||
@@ -142,8 +154,6 @@ LIGHT_COMMON_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_MIN_MIREDS): cv.template,
|
||||
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_RGB_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_RGB): cv.template,
|
||||
vol.Optional(CONF_RGBW_ACTION): cv.SCRIPT_SCHEMA,
|
||||
@@ -226,6 +236,7 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_LIGHTS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -242,6 +253,7 @@ async def async_setup_entry(
|
||||
StateLightEntity,
|
||||
LIGHT_CONFIG_ENTRY_SCHEMA,
|
||||
True,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -64,6 +64,13 @@ LEGACY_FIELDS = {
|
||||
CONF_VALUE_TEMPLATE: CONF_STATE,
|
||||
}
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_LOCK,
|
||||
CONF_OPEN,
|
||||
CONF_UNLOCK,
|
||||
)
|
||||
|
||||
|
||||
LOCK_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_CODE_FORMAT): cv.template,
|
||||
@@ -112,6 +119,7 @@ async def async_setup_platform(
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -127,6 +135,7 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateLockEntity,
|
||||
LOCK_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ CONF_SET_VALUE = "set_value"
|
||||
DEFAULT_NAME = "Template Number"
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
|
||||
SCRIPT_FIELDS = (CONF_SET_VALUE,)
|
||||
|
||||
NUMBER_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): cv.template,
|
||||
@@ -81,6 +83,7 @@ async def async_setup_platform(
|
||||
TriggerNumberEntity,
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -96,6 +99,7 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateNumberEntity,
|
||||
NUMBER_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -47,6 +47,8 @@ CONF_SELECT_OPTION = "select_option"
|
||||
|
||||
DEFAULT_NAME = "Template Select"
|
||||
|
||||
SCRIPT_FIELDS = (CONF_SELECT_OPTION,)
|
||||
|
||||
SELECT_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_OPTIONS): cv.template,
|
||||
@@ -79,6 +81,7 @@ async def async_setup_platform(
|
||||
TriggerSelectEntity,
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -94,6 +97,7 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
TemplateSelect,
|
||||
SELECT_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -57,11 +57,16 @@ LEGACY_FIELDS = {
|
||||
|
||||
DEFAULT_NAME = "Template Switch"
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_TURN_OFF,
|
||||
CONF_TURN_ON,
|
||||
)
|
||||
|
||||
SWITCH_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_STATE): cv.template,
|
||||
vol.Optional(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_TURN_OFF): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -109,6 +114,7 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_SWITCHES,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -125,6 +131,7 @@ async def async_setup_entry(
|
||||
StateSwitchEntity,
|
||||
SWITCH_CONFIG_ENTRY_SCHEMA,
|
||||
True,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -65,6 +65,8 @@ CONF_SPECIFIC_VERSION = "specific_version"
|
||||
CONF_TITLE = "title"
|
||||
CONF_UPDATE_PERCENTAGE = "update_percentage"
|
||||
|
||||
SCRIPT_FIELDS = (CONF_INSTALL,)
|
||||
|
||||
UPDATE_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_BACKUP, default=False): cv.boolean,
|
||||
@@ -105,6 +107,7 @@ async def async_setup_platform(
|
||||
TriggerUpdateEntity,
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -120,6 +123,7 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateUpdateEntity,
|
||||
UPDATE_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -76,6 +76,16 @@ LEGACY_FIELDS = {
|
||||
CONF_VALUE_TEMPLATE: CONF_STATE,
|
||||
}
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
SERVICE_CLEAN_SPOT,
|
||||
SERVICE_LOCATE,
|
||||
SERVICE_PAUSE,
|
||||
SERVICE_RETURN_TO_BASE,
|
||||
SERVICE_SET_FAN_SPEED,
|
||||
SERVICE_START,
|
||||
SERVICE_STOP,
|
||||
)
|
||||
|
||||
VACUUM_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_BATTERY_LEVEL): cv.template,
|
||||
@@ -150,6 +160,7 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_VACUUMS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -165,6 +176,7 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
TemplateStateVacuumEntity,
|
||||
VACUUM_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
"""The tests for template helpers."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_automation import toggle_entity
|
||||
from homeassistant.components.template.alarm_control_panel import (
|
||||
LEGACY_FIELDS as ALARM_CONTROL_PANEL_LEGACY_FIELDS,
|
||||
SCRIPT_FIELDS as ALARM_CONTROL_PANEL_SCRIPT_FIELDS,
|
||||
)
|
||||
from homeassistant.components.template.binary_sensor import (
|
||||
LEGACY_FIELDS as BINARY_SENSOR_LEGACY_FIELDS,
|
||||
)
|
||||
from homeassistant.components.template.button import StateButtonEntity
|
||||
from homeassistant.components.template.cover import LEGACY_FIELDS as COVER_LEGACY_FIELDS
|
||||
from homeassistant.components.template.fan import LEGACY_FIELDS as FAN_LEGACY_FIELDS
|
||||
from homeassistant.components.template.button import (
|
||||
SCRIPT_FIELDS as BUTTON_SCRIPT_FIELDS,
|
||||
StateButtonEntity,
|
||||
)
|
||||
from homeassistant.components.template.cover import (
|
||||
LEGACY_FIELDS as COVER_LEGACY_FIELDS,
|
||||
SCRIPT_FIELDS as COVER_SCRIPT_FIELDS,
|
||||
)
|
||||
from homeassistant.components.template.fan import (
|
||||
LEGACY_FIELDS as FAN_LEGACY_FIELDS,
|
||||
SCRIPT_FIELDS as FAN_SCRIPT_FIELDS,
|
||||
)
|
||||
from homeassistant.components.template.helpers import (
|
||||
async_setup_template_platform,
|
||||
create_legacy_template_issue,
|
||||
@@ -18,23 +32,56 @@ from homeassistant.components.template.helpers import (
|
||||
rewrite_legacy_to_modern_config,
|
||||
rewrite_legacy_to_modern_configs,
|
||||
)
|
||||
from homeassistant.components.template.light import LEGACY_FIELDS as LIGHT_LEGACY_FIELDS
|
||||
from homeassistant.components.template.lock import LEGACY_FIELDS as LOCK_LEGACY_FIELDS
|
||||
from homeassistant.components.template.light import (
|
||||
LEGACY_FIELDS as LIGHT_LEGACY_FIELDS,
|
||||
SCRIPT_FIELDS as LIGHT_SCRIPT_FIELDS,
|
||||
)
|
||||
from homeassistant.components.template.lock import (
|
||||
LEGACY_FIELDS as LOCK_LEGACY_FIELDS,
|
||||
SCRIPT_FIELDS as LOCK_SCRIPT_FIELDS,
|
||||
)
|
||||
from homeassistant.components.template.number import (
|
||||
SCRIPT_FIELDS as NUMBER_SCRIPT_FIELDS,
|
||||
)
|
||||
from homeassistant.components.template.select import (
|
||||
SCRIPT_FIELDS as SELECT_SCRIPT_FIELDS,
|
||||
)
|
||||
from homeassistant.components.template.sensor import (
|
||||
LEGACY_FIELDS as SENSOR_LEGACY_FIELDS,
|
||||
)
|
||||
from homeassistant.components.template.switch import (
|
||||
LEGACY_FIELDS as SWITCH_LEGACY_FIELDS,
|
||||
SCRIPT_FIELDS as SWITCH_SCRIPT_FIELDS,
|
||||
)
|
||||
from homeassistant.components.template.update import (
|
||||
SCRIPT_FIELDS as UPDATE_SCRIPT_FIELDS,
|
||||
)
|
||||
from homeassistant.components.template.vacuum import (
|
||||
LEGACY_FIELDS as VACUUM_LEGACY_FIELDS,
|
||||
SCRIPT_FIELDS as VACUUM_SCRIPT_FIELDS,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers import (
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .conftest import (
|
||||
ConfigurationStyle,
|
||||
TemplatePlatformSetup,
|
||||
assert_action,
|
||||
async_trigger,
|
||||
make_test_trigger,
|
||||
setup_entity,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry, mock_platform
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("legacy_fields", "old_attr", "new_attr", "attr_template"),
|
||||
@@ -327,6 +374,484 @@ async def test_legacy_to_modern_configs(
|
||||
] == altered_configs
|
||||
|
||||
|
||||
async def _setup_mock_devices(
|
||||
hass: HomeAssistant,
|
||||
domain: str,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> tuple[TemplatePlatformSetup, dr.DeviceEntry, er.RegistryEntry]:
|
||||
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, None, "test_entity", make_test_trigger("sensor.trigger")
|
||||
)
|
||||
return (platform_setup, device_entry, entity_entry)
|
||||
|
||||
|
||||
async def _setup_and_test_yaml_device_action(
|
||||
hass: HomeAssistant,
|
||||
style: ConfigurationStyle,
|
||||
domain: str,
|
||||
script_fields,
|
||||
extra_config: ConfigType,
|
||||
test_actions: tuple[tuple[str, dict], ...],
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
calls: list,
|
||||
) -> None:
|
||||
|
||||
platform_setup, device_entry, entity_entry = await _setup_mock_devices(
|
||||
hass, domain, device_registry, entity_registry
|
||||
)
|
||||
|
||||
actions = {
|
||||
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 script_fields
|
||||
}
|
||||
|
||||
await setup_entity(hass, platform_setup, style, 1, {**actions, **extra_config})
|
||||
await async_trigger(hass, "sensor.trigger", "anything")
|
||||
|
||||
for test_action, action_data in test_actions:
|
||||
call_count = len(calls)
|
||||
await hass.services.async_call(
|
||||
domain,
|
||||
test_action,
|
||||
{"entity_id": platform_setup.entity_id, **action_data},
|
||||
blocking=True,
|
||||
)
|
||||
assert_action(platform_setup, calls, call_count + 1, "fake_action")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"style",
|
||||
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("domain", "script_fields", "extra_config", "test_actions"),
|
||||
[
|
||||
(
|
||||
"alarm_control_panel",
|
||||
ALARM_CONTROL_PANEL_SCRIPT_FIELDS,
|
||||
{},
|
||||
(
|
||||
("alarm_arm_home", {"code": "1234"}),
|
||||
("alarm_arm_away", {"code": "1234"}),
|
||||
("alarm_arm_night", {"code": "1234"}),
|
||||
("alarm_arm_vacation", {"code": "1234"}),
|
||||
("alarm_arm_custom_bypass", {"code": "1234"}),
|
||||
("alarm_disarm", {"code": "1234"}),
|
||||
("alarm_trigger", {"code": "1234"}),
|
||||
),
|
||||
),
|
||||
(
|
||||
"cover",
|
||||
COVER_SCRIPT_FIELDS,
|
||||
{},
|
||||
(
|
||||
("open_cover", {}),
|
||||
("close_cover", {}),
|
||||
("stop_cover", {}),
|
||||
("set_cover_position", {"position": 25}),
|
||||
("set_cover_tilt_position", {"tilt_position": 25}),
|
||||
),
|
||||
),
|
||||
(
|
||||
"fan",
|
||||
FAN_SCRIPT_FIELDS,
|
||||
{
|
||||
"preset_modes": ["auto", "low", "medium", "high"],
|
||||
},
|
||||
(
|
||||
("turn_on", {}),
|
||||
("turn_off", {}),
|
||||
("set_percentage", {"percentage": 25}),
|
||||
("set_preset_mode", {"preset_mode": "auto"}),
|
||||
("oscillate", {"oscillating": True}),
|
||||
("set_direction", {"direction": "forward"}),
|
||||
),
|
||||
),
|
||||
(
|
||||
"light",
|
||||
LIGHT_SCRIPT_FIELDS,
|
||||
{"effect_list": "{{ ['foo', 'bar'] }}", "effect": "{{ 'foo' }}"},
|
||||
(
|
||||
("turn_on", {"brightness": 1}),
|
||||
("turn_off", {}),
|
||||
("turn_on", {"color_temp_kelvin": 8130}),
|
||||
("turn_on", {"hs_color": (360, 100)}),
|
||||
("turn_on", {"rgb_color": (160, 78, 192)}),
|
||||
("turn_on", {"rgbw_color": (160, 78, 192, 25)}),
|
||||
("turn_on", {"rgbww_color": (160, 78, 192, 25, 55)}),
|
||||
("turn_on", {"effect": "foo"}),
|
||||
),
|
||||
),
|
||||
(
|
||||
"lock",
|
||||
LOCK_SCRIPT_FIELDS,
|
||||
{},
|
||||
(
|
||||
("lock", {}),
|
||||
("unlock", {}),
|
||||
("open", {}),
|
||||
),
|
||||
),
|
||||
(
|
||||
"number",
|
||||
NUMBER_SCRIPT_FIELDS,
|
||||
{"step": "1"},
|
||||
(("set_value", {"value": 4}),),
|
||||
),
|
||||
(
|
||||
"select",
|
||||
SELECT_SCRIPT_FIELDS,
|
||||
{
|
||||
"options": "{{ ['test', 'yes', 'no'] }}",
|
||||
},
|
||||
(("select_option", {"option": "test"}),),
|
||||
),
|
||||
(
|
||||
"switch",
|
||||
SWITCH_SCRIPT_FIELDS,
|
||||
{},
|
||||
(
|
||||
("turn_on", {}),
|
||||
("turn_off", {}),
|
||||
),
|
||||
),
|
||||
(
|
||||
"update",
|
||||
UPDATE_SCRIPT_FIELDS,
|
||||
{"installed_version": "{{ '2.0.0' }}", "latest_version": "{{ '3.0.0' }}"},
|
||||
(("install", {}),),
|
||||
),
|
||||
(
|
||||
"vacuum",
|
||||
VACUUM_SCRIPT_FIELDS,
|
||||
{"fan_speeds": ["low", "medium", "high"]},
|
||||
(
|
||||
("start", {}),
|
||||
("pause", {}),
|
||||
("stop", {}),
|
||||
("return_to_base", {}),
|
||||
("clean_spot", {}),
|
||||
("locate", {}),
|
||||
("set_fan_speed", {"fan_speed": "medium"}),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_yaml_device_actions(
|
||||
hass: HomeAssistant,
|
||||
style: ConfigurationStyle,
|
||||
domain: str,
|
||||
script_fields,
|
||||
extra_config: ConfigType,
|
||||
test_actions: tuple[tuple[str, dict], ...],
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
calls: list,
|
||||
) -> None:
|
||||
"""Test device actions in platforms that support both trigger and modern configurations."""
|
||||
await _setup_and_test_yaml_device_action(
|
||||
hass,
|
||||
style,
|
||||
domain,
|
||||
script_fields,
|
||||
extra_config,
|
||||
test_actions,
|
||||
device_registry,
|
||||
entity_registry,
|
||||
calls,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"style",
|
||||
[ConfigurationStyle.MODERN],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("domain", "script_fields", "extra_config", "test_actions"),
|
||||
[
|
||||
(
|
||||
"button",
|
||||
BUTTON_SCRIPT_FIELDS,
|
||||
{},
|
||||
(("press", {}),),
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_yaml_device_actions_modern_config(
|
||||
hass: HomeAssistant,
|
||||
style: ConfigurationStyle,
|
||||
domain: str,
|
||||
script_fields,
|
||||
extra_config: str,
|
||||
test_actions: tuple[tuple[str, dict], ...],
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
calls: list,
|
||||
) -> None:
|
||||
"""Test device actions in platforms that supports modern configuration only."""
|
||||
await _setup_and_test_yaml_device_action(
|
||||
hass,
|
||||
style,
|
||||
domain,
|
||||
script_fields,
|
||||
extra_config,
|
||||
test_actions,
|
||||
device_registry,
|
||||
entity_registry,
|
||||
calls,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("domain", "script_fields", "extra_config", "test_actions"),
|
||||
[
|
||||
(
|
||||
"alarm_control_panel",
|
||||
ALARM_CONTROL_PANEL_SCRIPT_FIELDS,
|
||||
{"state": "{{ 'armed' }}"},
|
||||
(
|
||||
("alarm_arm_home", {"code": "1234"}),
|
||||
("alarm_arm_away", {"code": "1234"}),
|
||||
("alarm_arm_night", {"code": "1234"}),
|
||||
("alarm_arm_vacation", {"code": "1234"}),
|
||||
("alarm_arm_custom_bypass", {"code": "1234"}),
|
||||
("alarm_disarm", {"code": "1234"}),
|
||||
("alarm_trigger", {"code": "1234"}),
|
||||
),
|
||||
),
|
||||
(
|
||||
"button",
|
||||
BUTTON_SCRIPT_FIELDS,
|
||||
{},
|
||||
(("press", {}),),
|
||||
),
|
||||
(
|
||||
"cover",
|
||||
COVER_SCRIPT_FIELDS,
|
||||
{"state": "{{ 'open' }}"},
|
||||
(
|
||||
("open_cover", {}),
|
||||
("close_cover", {}),
|
||||
("stop_cover", {}),
|
||||
("set_cover_position", {"position": 25}),
|
||||
("set_cover_tilt_position", {"tilt_position": 25}),
|
||||
),
|
||||
),
|
||||
(
|
||||
"fan",
|
||||
FAN_SCRIPT_FIELDS,
|
||||
{
|
||||
"preset_modes": ["auto", "low", "medium", "high"],
|
||||
"state": "{{ 'on' }}",
|
||||
},
|
||||
(
|
||||
("turn_on", {}),
|
||||
("turn_off", {}),
|
||||
("set_percentage", {"percentage": 25}),
|
||||
("set_preset_mode", {"preset_mode": "auto"}),
|
||||
("oscillate", {"oscillating": True}),
|
||||
("set_direction", {"direction": "forward"}),
|
||||
),
|
||||
),
|
||||
(
|
||||
"light",
|
||||
LIGHT_SCRIPT_FIELDS,
|
||||
{
|
||||
"effect_list": "{{ ['foo', 'bar'] }}",
|
||||
"effect": "{{ 'foo' }}",
|
||||
"state": "{{ 'on' }}",
|
||||
},
|
||||
(
|
||||
("turn_on", {"brightness": 1}),
|
||||
("turn_off", {}),
|
||||
("turn_on", {"color_temp_kelvin": 8130}),
|
||||
("turn_on", {"hs_color": (360, 100)}),
|
||||
("turn_on", {"rgb_color": (160, 78, 192)}),
|
||||
("turn_on", {"rgbw_color": (160, 78, 192, 25)}),
|
||||
("turn_on", {"rgbww_color": (160, 78, 192, 25, 55)}),
|
||||
("turn_on", {"effect": "foo"}),
|
||||
),
|
||||
),
|
||||
(
|
||||
"lock",
|
||||
LOCK_SCRIPT_FIELDS,
|
||||
{
|
||||
"state": "{{ 'on' }}",
|
||||
},
|
||||
(
|
||||
("lock", {}),
|
||||
("unlock", {}),
|
||||
("open", {}),
|
||||
),
|
||||
),
|
||||
(
|
||||
"number",
|
||||
NUMBER_SCRIPT_FIELDS,
|
||||
{"step": 1},
|
||||
(("set_value", {"value": 4}),),
|
||||
),
|
||||
(
|
||||
"select",
|
||||
SELECT_SCRIPT_FIELDS,
|
||||
{
|
||||
"state": "{{ 'yes' }}",
|
||||
"options": "{{ ['test', 'yes', 'no'] }}",
|
||||
},
|
||||
(("select_option", {"option": "test"}),),
|
||||
),
|
||||
(
|
||||
"switch",
|
||||
SWITCH_SCRIPT_FIELDS,
|
||||
{
|
||||
"state": "{{ 'on' }}",
|
||||
},
|
||||
(
|
||||
("turn_on", {}),
|
||||
("turn_off", {}),
|
||||
),
|
||||
),
|
||||
(
|
||||
"update",
|
||||
UPDATE_SCRIPT_FIELDS,
|
||||
{"installed_version": "{{ '2.0.0' }}", "latest_version": "{{ '3.0.0' }}"},
|
||||
(("install", {}),),
|
||||
),
|
||||
(
|
||||
"vacuum",
|
||||
VACUUM_SCRIPT_FIELDS,
|
||||
{
|
||||
"fan_speeds": ["low", "medium", "high"],
|
||||
"state": "{{ 'on' }}",
|
||||
},
|
||||
(
|
||||
("start", {}),
|
||||
("pause", {}),
|
||||
("stop", {}),
|
||||
("return_to_base", {}),
|
||||
("clean_spot", {}),
|
||||
("locate", {}),
|
||||
("set_fan_speed", {"fan_speed": "medium"}),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_config_entry_device_actions(
|
||||
hass: HomeAssistant,
|
||||
domain: str,
|
||||
script_fields,
|
||||
extra_config: str,
|
||||
test_actions: tuple[tuple[str, dict], ...],
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
calls: list,
|
||||
) -> None:
|
||||
"""Test device actions in config flow."""
|
||||
|
||||
platform_setup, device_entry, entity_entry = await _setup_mock_devices(
|
||||
hass, domain, device_registry, entity_registry
|
||||
)
|
||||
|
||||
actions = {
|
||||
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 script_fields
|
||||
}
|
||||
|
||||
template_config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain="template",
|
||||
options={
|
||||
"name": platform_setup.object_id,
|
||||
"template_type": domain,
|
||||
**actions,
|
||||
**extra_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()
|
||||
|
||||
for test_action, action_data in test_actions:
|
||||
call_count = len(calls)
|
||||
await hass.services.async_call(
|
||||
domain,
|
||||
test_action,
|
||||
{"entity_id": platform_setup.entity_id, **action_data},
|
||||
blocking=True,
|
||||
)
|
||||
assert_action(platform_setup, calls, call_count + 1, "fake_action")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("domain", "legacy_fields"),
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user