diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 856060f8c75..6243c11a791 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -18,6 +18,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, ATTR_NAME, + CONF_ACTIONS, CONF_ALIAS, CONF_CONDITIONS, CONF_DEVICE_ID, @@ -27,6 +28,7 @@ from homeassistant.const import ( CONF_MODE, CONF_PATH, CONF_PLATFORM, + CONF_TRIGGERS, CONF_VARIABLES, CONF_ZONE, EVENT_HOMEASSISTANT_STARTED, @@ -86,11 +88,9 @@ from homeassistant.util.hass_dict import HassKey from .config import AutomationConfig, ValidationStatus from .const import ( - CONF_ACTIONS, CONF_INITIAL_STATE, CONF_TRACE, CONF_TRIGGER_VARIABLES, - CONF_TRIGGERS, DEFAULT_INITIAL_STATE, DOMAIN, LOGGER, diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index c4425ce099a..23ae10eea2b 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -14,11 +14,15 @@ from homeassistant.components import blueprint from homeassistant.components.trace import TRACE_CONFIG_SCHEMA from homeassistant.config import config_per_platform, config_without_domain from homeassistant.const import ( + CONF_ACTION, + CONF_ACTIONS, CONF_ALIAS, CONF_CONDITION, CONF_CONDITIONS, CONF_DESCRIPTION, CONF_ID, + CONF_TRIGGER, + CONF_TRIGGERS, CONF_VARIABLES, ) from homeassistant.core import HomeAssistant @@ -30,14 +34,10 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.util.yaml.input import UndefinedSubstitution from .const import ( - CONF_ACTION, - CONF_ACTIONS, CONF_HIDE_ENTITY, CONF_INITIAL_STATE, CONF_TRACE, - CONF_TRIGGER, CONF_TRIGGER_VARIABLES, - CONF_TRIGGERS, DOMAIN, LOGGER, ) diff --git a/homeassistant/components/automation/const.py b/homeassistant/components/automation/const.py index c4ac636282e..f9d2fc1b77f 100644 --- a/homeassistant/components/automation/const.py +++ b/homeassistant/components/automation/const.py @@ -2,10 +2,6 @@ import logging -CONF_ACTION = "action" -CONF_ACTIONS = "actions" -CONF_TRIGGER = "trigger" -CONF_TRIGGERS = "triggers" CONF_TRIGGER_VARIABLES = "trigger_variables" DOMAIN = "automation" diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index 15a73cf3de5..c3f832b0c54 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -12,6 +12,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE_ID, CONF_NAME, + CONF_TRIGGERS, CONF_UNIQUE_ID, SERVICE_RELOAD, ) @@ -27,7 +28,7 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration from homeassistant.util.hass_dict import HassKey -from .const import CONF_MAX, CONF_MIN, CONF_STEP, CONF_TRIGGER, DOMAIN, PLATFORMS +from .const import CONF_MAX, CONF_MIN, CONF_STEP, DOMAIN, PLATFORMS from .coordinator import TriggerUpdateCoordinator from .helpers import async_get_blueprints @@ -136,7 +137,7 @@ async def _process_config(hass: HomeAssistant, hass_config: ConfigType) -> None: coordinator_tasks: list[Coroutine[Any, Any, TriggerUpdateCoordinator]] = [] for conf_section in hass_config[DOMAIN]: - if CONF_TRIGGER in conf_section: + if CONF_TRIGGERS in conf_section: coordinator_tasks.append(init_coordinator(hass, conf_section)) continue diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index bce1d2764d7..5038114b8ab 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -3,6 +3,7 @@ from collections.abc import Callable from contextlib import suppress import logging +from typing import Any import voluptuous as vol @@ -10,6 +11,7 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAI from homeassistant.components.blueprint import ( BLUEPRINT_INSTANCE_FIELDS, is_blueprint_instance_config, + schemas as blueprint_schemas, ) from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.cover import DOMAIN as COVER_DOMAIN @@ -22,9 +24,15 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN from homeassistant.config import async_log_schema_error, config_without_domain from homeassistant.const import ( + CONF_ACTION, + CONF_ACTIONS, CONF_BINARY_SENSORS, + CONF_CONDITION, + CONF_CONDITIONS, CONF_NAME, CONF_SENSORS, + CONF_TRIGGER, + CONF_TRIGGERS, CONF_UNIQUE_ID, CONF_VARIABLES, ) @@ -47,14 +55,7 @@ from . import ( switch as switch_platform, weather as weather_platform, ) -from .const import ( - CONF_ACTION, - CONF_CONDITION, - CONF_TRIGGER, - DOMAIN, - PLATFORMS, - TemplateConfig, -) +from .const import DOMAIN, PLATFORMS, TemplateConfig from .helpers import async_get_blueprints PACKAGE_MERGE_HINT = "list" @@ -67,7 +68,7 @@ def ensure_domains_do_not_have_trigger_or_action(*keys: str) -> Callable[[dict], def validate(obj: dict): options = set(obj.keys()) if found_domains := domains.intersection(options): - invalid = {CONF_TRIGGER, CONF_ACTION} + invalid = {CONF_TRIGGERS, CONF_ACTIONS} if found_invalid := invalid.intersection(set(obj.keys())): raise vol.Invalid( f"Unsupported option(s) found for domain {found_domains.pop()}, please remove ({', '.join(found_invalid)}) from your configuration", @@ -78,13 +79,22 @@ def ensure_domains_do_not_have_trigger_or_action(*keys: str) -> Callable[[dict], return validate -CONFIG_SECTION_SCHEMA = vol.Schema( - vol.All( +def _backward_compat_schema(value: Any | None) -> Any: + """Backward compatibility for automations.""" + + value = cv.renamed(CONF_TRIGGER, CONF_TRIGGERS)(value) + value = cv.renamed(CONF_ACTION, CONF_ACTIONS)(value) + return cv.renamed(CONF_CONDITION, CONF_CONDITIONS)(value) + + +CONFIG_SECTION_SCHEMA = vol.All( + _backward_compat_schema, + vol.Schema( { vol.Optional(CONF_UNIQUE_ID): cv.string, - vol.Optional(CONF_TRIGGER): cv.TRIGGER_SCHEMA, - vol.Optional(CONF_CONDITION): cv.CONDITIONS_SCHEMA, - vol.Optional(CONF_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_TRIGGERS): cv.TRIGGER_SCHEMA, + vol.Optional(CONF_CONDITIONS): cv.CONDITIONS_SCHEMA, + vol.Optional(CONF_ACTIONS): cv.SCRIPT_SCHEMA, vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA, vol.Optional(NUMBER_DOMAIN): vol.All( cv.ensure_list, [number_platform.NUMBER_SCHEMA] @@ -123,10 +133,14 @@ CONFIG_SECTION_SCHEMA = vol.Schema( cv.ensure_list, [cover_platform.COVER_SCHEMA] ), }, - ensure_domains_do_not_have_trigger_or_action( - BUTTON_DOMAIN, COVER_DOMAIN, LIGHT_DOMAIN - ), - ) + ), + ensure_domains_do_not_have_trigger_or_action( + BUTTON_DOMAIN, COVER_DOMAIN, LIGHT_DOMAIN + ), +) + +TEMPLATE_BLUEPRINT_SCHEMA = vol.All( + _backward_compat_schema, blueprint_schemas.BLUEPRINT_SCHEMA ) TEMPLATE_BLUEPRINT_INSTANCE_SCHEMA = vol.Schema( @@ -169,7 +183,7 @@ async def _async_resolve_blueprints( # house input results for template entities. For Trigger based template entities # CONF_VARIABLES should not be removed because the variables are always # executed between the trigger and action. - if CONF_TRIGGER not in config and CONF_VARIABLES in config: + if CONF_TRIGGERS not in config and CONF_VARIABLES in config: config[platform][CONF_VARIABLES] = config.pop(CONF_VARIABLES) raw_config = dict(config) @@ -187,14 +201,14 @@ async def async_validate_config_section( validated_config = await _async_resolve_blueprints(hass, config) - if CONF_TRIGGER in validated_config: - validated_config[CONF_TRIGGER] = await async_validate_trigger_config( - hass, validated_config[CONF_TRIGGER] + if CONF_TRIGGERS in validated_config: + validated_config[CONF_TRIGGERS] = await async_validate_trigger_config( + hass, validated_config[CONF_TRIGGERS] ) - if CONF_CONDITION in validated_config: - validated_config[CONF_CONDITION] = await async_validate_conditions_config( - hass, validated_config[CONF_CONDITION] + if CONF_CONDITIONS in validated_config: + validated_config[CONF_CONDITIONS] = await async_validate_conditions_config( + hass, validated_config[CONF_CONDITIONS] ) return validated_config diff --git a/homeassistant/components/template/const.py b/homeassistant/components/template/const.py index f333d14797e..53c0fa3af13 100644 --- a/homeassistant/components/template/const.py +++ b/homeassistant/components/template/const.py @@ -1,22 +1,18 @@ """Constants for the Template Platform Components.""" -from homeassistant.components.blueprint import BLUEPRINT_SCHEMA from homeassistant.const import Platform from homeassistant.helpers.typing import ConfigType -CONF_ACTION = "action" CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" CONF_ATTRIBUTES = "attributes" CONF_AVAILABILITY = "availability" CONF_AVAILABILITY_TEMPLATE = "availability_template" -CONF_CONDITION = "condition" CONF_MAX = "max" CONF_MIN = "min" CONF_OBJECT_ID = "object_id" CONF_PICTURE = "picture" CONF_PRESS = "press" CONF_STEP = "step" -CONF_TRIGGER = "trigger" CONF_TURN_OFF = "turn_off" CONF_TURN_ON = "turn_on" @@ -41,8 +37,6 @@ PLATFORMS = [ Platform.WEATHER, ] -TEMPLATE_BLUEPRINT_SCHEMA = BLUEPRINT_SCHEMA - class TemplateConfig(dict): """Dummy class to allow adding attributes.""" diff --git a/homeassistant/components/template/coordinator.py b/homeassistant/components/template/coordinator.py index c11e9b6101b..a2823233336 100644 --- a/homeassistant/components/template/coordinator.py +++ b/homeassistant/components/template/coordinator.py @@ -5,7 +5,14 @@ import logging from typing import TYPE_CHECKING, Any, cast from homeassistant.components.blueprint import CONF_USE_BLUEPRINT -from homeassistant.const import CONF_PATH, CONF_VARIABLES, EVENT_HOMEASSISTANT_START +from homeassistant.const import ( + CONF_ACTIONS, + CONF_CONDITIONS, + CONF_PATH, + CONF_TRIGGERS, + CONF_VARIABLES, + EVENT_HOMEASSISTANT_START, +) from homeassistant.core import Context, CoreState, Event, HomeAssistant, callback from homeassistant.helpers import condition, discovery, trigger as trigger_helper from homeassistant.helpers.script import Script @@ -14,7 +21,7 @@ from homeassistant.helpers.trace import trace_get from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import CONF_ACTION, CONF_CONDITION, CONF_TRIGGER, DOMAIN, PLATFORMS +from .const import DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) @@ -84,17 +91,17 @@ class TriggerUpdateCoordinator(DataUpdateCoordinator): async def _attach_triggers(self, start_event: Event | None = None) -> None: """Attach the triggers.""" - if CONF_ACTION in self.config: + if CONF_ACTIONS in self.config: self._script = Script( self.hass, - self.config[CONF_ACTION], + self.config[CONF_ACTIONS], self.name, DOMAIN, ) - if CONF_CONDITION in self.config: + if CONF_CONDITIONS in self.config: self._cond_func = await condition.async_conditions_from_config( - self.hass, self.config[CONF_CONDITION], _LOGGER, "template entity" + self.hass, self.config[CONF_CONDITIONS], _LOGGER, "template entity" ) if start_event is not None: @@ -107,7 +114,7 @@ class TriggerUpdateCoordinator(DataUpdateCoordinator): self._unsub_trigger = await trigger_helper.async_initialize_triggers( self.hass, - self.config[CONF_TRIGGER], + self.config[CONF_TRIGGERS], action, DOMAIN, self.name, diff --git a/homeassistant/components/template/helpers.py b/homeassistant/components/template/helpers.py index d74a4a4ed00..660227f65dc 100644 --- a/homeassistant/components/template/helpers.py +++ b/homeassistant/components/template/helpers.py @@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import async_get_platforms from homeassistant.helpers.singleton import singleton -from .const import DOMAIN, TEMPLATE_BLUEPRINT_SCHEMA +from .const import DOMAIN from .entity import AbstractTemplateEntity DATA_BLUEPRINTS = "template_blueprints" @@ -54,6 +54,9 @@ async def _reload_blueprint_templates(hass: HomeAssistant, blueprint_path: str) @callback def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints: """Get template blueprints.""" + # pylint: disable-next=import-outside-toplevel + from .config import TEMPLATE_BLUEPRINT_SCHEMA + return blueprint.DomainBlueprints( hass, DOMAIN, diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index ca3736ebf76..508c8b2aed4 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -33,6 +33,8 @@ from homeassistant.const import ( CONF_NAME, CONF_SENSORS, CONF_STATE, + CONF_TRIGGER, + CONF_TRIGGERS, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, @@ -53,12 +55,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util from . import TriggerUpdateCoordinator -from .const import ( - CONF_ATTRIBUTE_TEMPLATES, - CONF_AVAILABILITY_TEMPLATE, - CONF_OBJECT_ID, - CONF_TRIGGER, -) +from .const import CONF_ATTRIBUTE_TEMPLATES, CONF_AVAILABILITY_TEMPLATE, CONF_OBJECT_ID from .template_entity import ( TEMPLATE_ENTITY_COMMON_SCHEMA, TemplateEntity, @@ -132,7 +129,7 @@ LEGACY_SENSOR_SCHEMA = vol.All( def extra_validation_checks(val): """Run extra validation checks.""" - if CONF_TRIGGER in val: + if CONF_TRIGGERS in val or CONF_TRIGGER in val: raise vol.Invalid( "You can only add triggers to template entities if they are defined under" " `template:`. See the template documentation for more information:" @@ -170,6 +167,7 @@ PLATFORM_SCHEMA = vol.All( SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_TRIGGER): cv.match_all, # to raise custom warning + vol.Optional(CONF_TRIGGERS): cv.match_all, # to raise custom warning vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(LEGACY_SENSOR_SCHEMA), } ), diff --git a/homeassistant/const.py b/homeassistant/const.py index 64faf019567..b73aed1b8b9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -115,6 +115,7 @@ SUN_EVENT_SUNRISE: Final = "sunrise" CONF_ABOVE: Final = "above" CONF_ACCESS_TOKEN: Final = "access_token" CONF_ACTION: Final = "action" +CONF_ACTIONS: Final = "actions" CONF_ADDRESS: Final = "address" CONF_AFTER: Final = "after" CONF_ALIAS: Final = "alias" diff --git a/tests/components/template/test_blueprint.py b/tests/components/template/test_blueprint.py index 66630ecf739..43f2c310289 100644 --- a/tests/components/template/test_blueprint.py +++ b/tests/components/template/test_blueprint.py @@ -212,11 +212,16 @@ async def test_reload_template_when_blueprint_changes(hass: HomeAssistant) -> No assert not_inverted.state == "on" +@pytest.mark.parametrize( + ("blueprint"), + ["test_event_sensor.yaml", "test_event_sensor_legacy_schema.yaml"], +) async def test_trigger_event_sensor( - hass: HomeAssistant, device_registry: dr.DeviceRegistry + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + blueprint: str, ) -> None: """Test event sensor blueprint.""" - blueprint = "test_event_sensor.yaml" assert await async_setup_component( hass, "template", diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index e7af5296d4e..56eaa120b20 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -2303,6 +2303,61 @@ async def test_trigger_conditional_action(hass: HomeAssistant) -> None: assert len(events) == 1 +@pytest.mark.parametrize("trigger_field", ["trigger", "triggers"]) +@pytest.mark.parametrize("condition_field", ["condition", "conditions"]) +@pytest.mark.parametrize("action_field", ["action", "actions"]) +async def test_legacy_and_new_config_schema( + hass: HomeAssistant, trigger_field: str, condition_field: str, action_field: str +) -> None: + """Tests that both old and new config schema (singular -> plural) work.""" + + assert await async_setup_component( + hass, + "template", + { + "template": [ + { + "unique_id": "listening-test-event", + f"{trigger_field}": { + "platform": "event", + "event_type": "beer_event", + }, + f"{condition_field}": [ + { + "condition": "template", + "value_template": "{{ trigger.event.data.beer >= 42 }}", + } + ], + f"{action_field}": [ + {"event": "test_event_by_action"}, + ], + "sensor": [ + { + "name": "Unimportant", + "state": "Uninteresting", + } + ], + }, + ], + }, + ) + + await hass.async_block_till_done() + + event = "test_event_by_action" + events = async_capture_events(hass, event) + + hass.bus.async_fire("beer_event", {"beer": 1}) + await hass.async_block_till_done() + + assert len(events) == 0 + + hass.bus.async_fire("beer_event", {"beer": 42}) + await hass.async_block_till_done() + + assert len(events) == 1 + + async def test_device_id( hass: HomeAssistant, device_registry: dr.DeviceRegistry, diff --git a/tests/testing_config/blueprints/template/test_event_sensor.yaml b/tests/testing_config/blueprints/template/test_event_sensor.yaml index 8b615eb90ba..2ce8519c8e9 100644 --- a/tests/testing_config/blueprints/template/test_event_sensor.yaml +++ b/tests/testing_config/blueprints/template/test_event_sensor.yaml @@ -14,7 +14,7 @@ blueprint: description: The event_data for the event trigger selector: object: -trigger: +triggers: - trigger: event event_type: !input event_type event_data: !input event_data diff --git a/tests/testing_config/blueprints/template/test_event_sensor_legacy_schema.yaml b/tests/testing_config/blueprints/template/test_event_sensor_legacy_schema.yaml new file mode 100644 index 00000000000..8b615eb90ba --- /dev/null +++ b/tests/testing_config/blueprints/template/test_event_sensor_legacy_schema.yaml @@ -0,0 +1,27 @@ +blueprint: + name: Create Sensor from Event + description: Creates a timestamp sensor from an event + domain: template + source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/template/blueprints/event_sensor.yaml + input: + event_type: + name: Name of the event_type + description: The event_type for the event trigger + selector: + text: + event_data: + name: The data for the event + description: The event_data for the event trigger + selector: + object: +trigger: + - trigger: event + event_type: !input event_type + event_data: !input event_data +variables: + event_data: "{{ trigger.event.data }}" +sensor: + state: "{{ now() }}" + device_class: timestamp + attributes: + data: "{{ event_data }}"