Change trigger platform key to trigger (#124357)

* fix

* Fix

* Fix

* Update homeassistant/helpers/config_validation.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Fix

* Fix

* Fix

* Fix

* Add more tests

* Fix

* Fix tests

* Add tests

* Let's see what the CI does

* It fails on the code that tested the thing ofc

* It fails on the code that tested the thing ofc

* Revert test thingy

* Now the test works again, lovely

* Another one

* Fix websocket thingy

* Only copy when needed

* Improve comment

* Remove test

* Fix docstring

* I think this now also work since this transforms trigger to platform

* Add comment

* Update homeassistant/helpers/config_validation.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Update homeassistant/helpers/config_validation.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Update homeassistant/helpers/config_validation.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Check for mapping

* Add test

* Update homeassistant/helpers/config_validation.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Update test to also test for trigger keys

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Joost Lekkerkerker 2024-09-25 14:19:58 +02:00 committed by GitHub
parent 9d29307532
commit a1906b434f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 185 additions and 58 deletions

View file

@ -481,8 +481,11 @@ async def websocket_device_automation_get_condition_capabilities(
@websocket_api.websocket_command(
{
vol.Required("type"): "device_automation/trigger/capabilities",
vol.Required("trigger"): DEVICE_TRIGGER_BASE_SCHEMA.extend(
{}, extra=vol.ALLOW_EXTRA
# The frontend responds with `trigger` as key, while the
# `DEVICE_TRIGGER_BASE_SCHEMA` expects `platform1` as key.
vol.Required("trigger"): vol.All(
cv._backward_compat_trigger_schema, # noqa: SLF001
DEVICE_TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA),
),
}
)

View file

@ -282,6 +282,7 @@ CONF_THEN: Final = "then"
CONF_TIMEOUT: Final = "timeout"
CONF_TIME_ZONE: Final = "time_zone"
CONF_TOKEN: Final = "token"
CONF_TRIGGER: Final = "trigger"
CONF_TRIGGERS: Final = "triggers"
CONF_TRIGGER_TIME: Final = "trigger_time"
CONF_TTL: Final = "ttl"

View file

@ -4,7 +4,7 @@
# with PEP 695 syntax. Fixed in Python 3.13.
# from __future__ import annotations
from collections.abc import Callable, Hashable
from collections.abc import Callable, Hashable, Mapping
import contextlib
from contextvars import ContextVar
from datetime import (
@ -81,6 +81,7 @@ from homeassistant.const import (
CONF_TARGET,
CONF_THEN,
CONF_TIMEOUT,
CONF_TRIGGER,
CONF_TRIGGERS,
CONF_UNTIL,
CONF_VALUE_TEMPLATE,
@ -1769,6 +1770,30 @@ CONDITION_ACTION_SCHEMA: vol.Schema = vol.Schema(
)
)
def _backward_compat_trigger_schema(value: Any | None) -> Any:
"""Rewrite trigger `trigger` to `platform`.
`platform` has been renamed to `trigger` in user documentation and in the automation
editor. The Python trigger implementation still uses `platform`, so we need to
rename `trigger` to `platform.
"""
if not isinstance(value, Mapping):
# If the value is not a mapping, we let that be handled by the TRIGGER_SCHEMA
return value
if CONF_TRIGGER in value:
if CONF_PLATFORM in value:
raise vol.Invalid(
"Cannot specify both 'platform' and 'trigger'. Please use 'trigger' only."
)
value = dict(value)
value[CONF_PLATFORM] = value.pop(CONF_TRIGGER)
return value
TRIGGER_BASE_SCHEMA = vol.Schema(
{
vol.Optional(CONF_ALIAS): str,
@ -1804,7 +1829,9 @@ def _base_trigger_validator(value: Any) -> Any:
TRIGGER_SCHEMA = vol.All(
ensure_list, _base_trigger_list_flatten, [_base_trigger_validator]
ensure_list,
_base_trigger_list_flatten,
[vol.All(_backward_compat_trigger_schema, _base_trigger_validator)],
)
_SCRIPT_DELAY_SCHEMA = vol.Schema(

View file

@ -602,7 +602,7 @@ def _selector_serializer(schema: Any) -> Any: # noqa: C901
return {"type": "string", "format": "time"}
if isinstance(schema, selector.TriggerSelector):
return convert(cv.TRIGGER_SCHEMA)
return {"type": "array", "items": {"type": "string"}}
if schema.config.get("multiple"):
return {"type": "array", "items": {"type": "string"}}

View file

@ -1971,37 +1971,37 @@ async def test_extraction_functions(
{
"alias": "test1",
"triggers": [
{"platform": "state", "entity_id": "sensor.trigger_state"},
{"trigger": "state", "entity_id": "sensor.trigger_state"},
{
"platform": "numeric_state",
"trigger": "numeric_state",
"entity_id": "sensor.trigger_numeric_state",
"above": 10,
},
{
"platform": "calendar",
"trigger": "calendar",
"entity_id": "calendar.trigger_calendar",
"event": "start",
},
{
"platform": "event",
"trigger": "event",
"event_type": "state_changed",
"event_data": {"entity_id": "sensor.trigger_event"},
},
# entity_id is a list of strings (not supported)
{
"platform": "event",
"trigger": "event",
"event_type": "state_changed",
"event_data": {"entity_id": ["sensor.trigger_event2"]},
},
# entity_id is not a valid entity ID
{
"platform": "event",
"trigger": "event",
"event_type": "state_changed",
"event_data": {"entity_id": "abc"},
},
# entity_id is not a string
{
"platform": "event",
"trigger": "event",
"event_type": "state_changed",
"event_data": {"entity_id": 123},
},
@ -2044,36 +2044,36 @@ async def test_extraction_functions(
"alias": "test2",
"triggers": [
{
"platform": "device",
"trigger": "device",
"domain": "light",
"type": "turned_on",
"entity_id": "light.trigger_2",
"device_id": trigger_device_2.id,
},
{
"platform": "tag",
"trigger": "tag",
"tag_id": "1234",
"device_id": "device-trigger-tag1",
},
{
"platform": "tag",
"trigger": "tag",
"tag_id": "1234",
"device_id": ["device-trigger-tag2", "device-trigger-tag3"],
},
{
"platform": "event",
"trigger": "event",
"event_type": "esphome.button_pressed",
"event_data": {"device_id": "device-trigger-event"},
},
# device_id is a list of strings (not supported)
{
"platform": "event",
"trigger": "event",
"event_type": "esphome.button_pressed",
"event_data": {"device_id": ["device-trigger-event2"]},
},
# device_id is not a string
{
"platform": "event",
"trigger": "event",
"event_type": "esphome.button_pressed",
"event_data": {"device_id": 123},
},
@ -2114,19 +2114,19 @@ async def test_extraction_functions(
"alias": "test3",
"triggers": [
{
"platform": "event",
"trigger": "event",
"event_type": "esphome.button_pressed",
"event_data": {"area_id": "area-trigger-event"},
},
# area_id is a list of strings (not supported)
{
"platform": "event",
"trigger": "event",
"event_type": "esphome.button_pressed",
"event_data": {"area_id": ["area-trigger-event2"]},
},
# area_id is not a string
{
"platform": "event",
"trigger": "event",
"event_type": "esphome.button_pressed",
"event_data": {"area_id": 123},
},
@ -2287,7 +2287,7 @@ async def test_automation_variables(
"event_type": "{{ trigger.event.event_type }}",
"this_variables": "{{this.entity_id}}",
},
"triggers": {"platform": "event", "event_type": "test_event"},
"triggers": {"trigger": "event", "event_type": "test_event"},
"actions": {
"action": "test.automation",
"data": {
@ -2302,7 +2302,7 @@ async def test_automation_variables(
"variables": {
"test_var": "defined_in_config",
},
"trigger": {"platform": "event", "event_type": "test_event_2"},
"trigger": {"trigger": "event", "event_type": "test_event_2"},
"conditions": {
"condition": "template",
"value_template": "{{ trigger.event.data.pass_condition }}",
@ -2315,7 +2315,7 @@ async def test_automation_variables(
"variables": {
"test_var": "{{ trigger.event.data.break + 1 }}",
},
"triggers": {"platform": "event", "event_type": "test_event_3"},
"triggers": {"trigger": "event", "event_type": "test_event_3"},
"actions": {
"action": "test.automation",
},
@ -2371,7 +2371,7 @@ async def test_automation_trigger_variables(
"trigger_variables": {
"test_var": "defined_in_config",
},
"trigger": {"platform": "event", "event_type": "test_event"},
"trigger": {"trigger": "event", "event_type": "test_event"},
"action": {
"action": "test.automation",
"data": {
@ -2389,7 +2389,7 @@ async def test_automation_trigger_variables(
"test_var": "defined_in_config",
"this_trigger_variables": "{{this.entity_id}}",
},
"trigger": {"platform": "event", "event_type": "test_event_2"},
"trigger": {"trigger": "event", "event_type": "test_event_2"},
"action": {
"action": "test.automation",
"data": {
@ -2436,7 +2436,7 @@ async def test_automation_bad_trigger_variables(
"trigger_variables": {
"test_var": "{{ states('foo.bar') }}",
},
"trigger": {"platform": "event", "event_type": "test_event"},
"trigger": {"trigger": "event", "event_type": "test_event"},
"action": {
"action": "test.automation",
},
@ -2463,7 +2463,7 @@ async def test_automation_this_var_always(
{
automation.DOMAIN: [
{
"trigger": {"platform": "event", "event_type": "test_event"},
"trigger": {"trigger": "event", "event_type": "test_event"},
"action": {
"action": "test.automation",
"data": {
@ -2739,7 +2739,7 @@ async def test_trigger_service(hass: HomeAssistant, calls: list[ServiceCall]) ->
{
automation.DOMAIN: {
"alias": "hello",
"trigger": {"platform": "event", "event_type": "test_event"},
"trigger": {"trigger": "event", "event_type": "test_event"},
"action": {
"action": "test.automation",
"data_template": {"trigger": "{{ trigger }}"},
@ -2771,9 +2771,9 @@ async def test_trigger_condition_implicit_id(
{
automation.DOMAIN: {
"trigger": [
{"platform": "event", "event_type": "test_event1"},
{"platform": "event", "event_type": "test_event2"},
{"platform": "event", "event_type": "test_event3"},
{"trigger": "event", "event_type": "test_event1"},
{"trigger": "event", "event_type": "test_event2"},
{"trigger": "event", "event_type": "test_event3"},
],
"action": {
"choose": [
@ -2823,8 +2823,8 @@ async def test_trigger_condition_explicit_id(
{
automation.DOMAIN: {
"trigger": [
{"platform": "event", "event_type": "test_event1", "id": "one"},
{"platform": "event", "event_type": "test_event2", "id": "two"},
{"trigger": "event", "event_type": "test_event1", "id": "one"},
{"trigger": "event", "event_type": "test_event2", "id": "two"},
],
"action": {
"choose": [
@ -2938,7 +2938,7 @@ async def test_recursive_automation_starting_script(
automation.DOMAIN: {
"mode": automation_mode,
"trigger": [
{"platform": "event", "event_type": "trigger_automation"},
{"trigger": "event", "event_type": "trigger_automation"},
],
"action": [
{"action": "test.automation_started"},
@ -3020,7 +3020,7 @@ async def test_recursive_automation(
automation.DOMAIN: {
"mode": automation_mode,
"trigger": [
{"platform": "event", "event_type": "trigger_automation"},
{"trigger": "event", "event_type": "trigger_automation"},
],
"action": [
{"event": "trigger_automation"},
@ -3082,7 +3082,7 @@ async def test_recursive_automation_restart_mode(
automation.DOMAIN: {
"mode": SCRIPT_MODE_RESTART,
"trigger": [
{"platform": "event", "event_type": "trigger_automation"},
{"trigger": "event", "event_type": "trigger_automation"},
],
"action": [
{"event": "trigger_automation"},
@ -3121,7 +3121,7 @@ async def test_websocket_config(
"""Test config command."""
config = {
"alias": "hello",
"triggers": {"platform": "event", "event_type": "test_event"},
"triggers": {"trigger": "event", "event_type": "test_event"},
"actions": {"action": "test.automation", "data": 100},
}
assert await async_setup_component(
@ -3191,7 +3191,7 @@ async def test_automation_turns_off_other_automation(hass: HomeAssistant) -> Non
automation.DOMAIN: [
{
"trigger": {
"platform": "state",
"trigger": "state",
"entity_id": "binary_sensor.presence",
"from": "on",
},
@ -3209,7 +3209,7 @@ async def test_automation_turns_off_other_automation(hass: HomeAssistant) -> Non
},
{
"trigger": {
"platform": "state",
"trigger": "state",
"entity_id": "binary_sensor.presence",
"from": "on",
"for": {
@ -3302,7 +3302,7 @@ async def test_two_automations_call_restart_script_same_time(
automation.DOMAIN: [
{
"trigger": {
"platform": "state",
"trigger": "state",
"entity_id": "binary_sensor.presence",
"to": "on",
},
@ -3314,7 +3314,7 @@ async def test_two_automations_call_restart_script_same_time(
},
{
"trigger": {
"platform": "state",
"trigger": "state",
"entity_id": "binary_sensor.presence",
"to": "on",
},
@ -3360,7 +3360,7 @@ async def test_two_automation_call_restart_script_right_after_each_other(
automation.DOMAIN: [
{
"trigger": {
"platform": "state",
"trigger": "state",
"entity_id": ["input_boolean.test_1", "input_boolean.test_1"],
"from": "off",
"to": "on",
@ -3419,7 +3419,7 @@ async def test_action_backward_compatibility(
automation.DOMAIN,
{
automation.DOMAIN: {
"trigger": {"platform": "event", "event_type": "test_event"},
"trigger": {"trigger": "event", "event_type": "test_event"},
"condition": {
"condition": "template",
"value_template": "{{ True }}",
@ -3467,6 +3467,17 @@ async def test_action_backward_compatibility(
},
"Cannot specify both 'action' and 'actions'. Please use 'actions' only.",
),
(
{
"trigger": {
"platform": "event",
"trigger": "event",
"event_type": "test_event2",
},
"action": [],
},
"Cannot specify both 'platform' and 'trigger'. Please use 'trigger' only.",
),
],
)
async def test_invalid_configuration(
@ -3483,3 +3494,28 @@ async def test_invalid_configuration(
)
await hass.async_block_till_done()
assert message in caplog.text
@pytest.mark.parametrize(
("trigger_key"),
["trigger", "platform"],
)
async def test_valid_configuration(
hass: HomeAssistant,
trigger_key: str,
) -> None:
"""Test for valid automation configurations."""
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"triggers": {
trigger_key: "event",
"event_type": "test_event2",
},
"action": [],
}
},
)
await hass.async_block_till_done()

View file

@ -39,7 +39,7 @@ async def test_exclude_attributes(
automation.DOMAIN,
{
automation.DOMAIN: {
"trigger": {"platform": "event", "event_type": "test_event"},
"trigger": {"trigger": "event", "event_type": "test_event"},
"actions": {"action": "test.automation", "entity_id": "hello.world"},
}
},

View file

@ -224,7 +224,7 @@ async def test_save_blueprint(
" service_to_call:\n a_number:\n selector:\n number:\n "
" mode: box\n step: 1.0\n source_url:"
" https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntriggers:\n"
" platform: event\n event_type: !input 'trigger_event'\nactions:\n "
" trigger: event\n event_type: !input 'trigger_event'\nactions:\n "
" service: !input 'service_to_call'\n entity_id: light.kitchen\n"
# c dumper will not quote the value after !input
"blueprint:\n name: Call service based on event\n domain: automation\n "
@ -232,7 +232,7 @@ async def test_save_blueprint(
" service_to_call:\n a_number:\n selector:\n number:\n "
" mode: box\n step: 1.0\n source_url:"
" https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntriggers:\n"
" platform: event\n event_type: !input trigger_event\nactions:\n service:"
" trigger: event\n event_type: !input trigger_event\nactions:\n service:"
" !input service_to_call\n entity_id: light.kitchen\n"
)
# Make sure ita parsable and does not raise
@ -500,7 +500,7 @@ async def test_substituting_blueprint_inputs(
},
"triggers": {
"event_type": "test_event",
"platform": "event",
"trigger": "event",
},
}

View file

@ -110,14 +110,14 @@ async def test_update_automation_config(
),
(
{
"trigger": {"platform": "automation"},
"trigger": {"trigger": "automation"},
"action": [],
},
"Integration 'automation' does not provide trigger support",
),
(
{
"trigger": {"platform": "event", "event_type": "test_event"},
"trigger": {"trigger": "event", "event_type": "test_event"},
"condition": {
"condition": "state",
# The UUID will fail being resolved to en entity_id
@ -130,7 +130,7 @@ async def test_update_automation_config(
),
(
{
"trigger": {"platform": "event", "event_type": "test_event"},
"trigger": {"trigger": "event", "event_type": "test_event"},
"action": {
"condition": "state",
# The UUID will fail being resolved to en entity_id
@ -336,12 +336,12 @@ async def test_bad_formatted_automations(
[
{
"id": "sun",
"trigger": {"platform": "event", "event_type": "test_event"},
"trigger": {"trigger": "event", "event_type": "test_event"},
"action": {"service": "test.automation"},
},
{
"id": "moon",
"trigger": {"platform": "event", "event_type": "test_event"},
"trigger": {"trigger": "event", "event_type": "test_event"},
"action": {"service": "test.automation"},
},
],

View file

@ -720,12 +720,17 @@ async def test_async_get_device_automations_all_devices_action_exception_throw(
assert "KeyError" in caplog.text
@pytest.mark.parametrize(
"trigger_key",
["trigger", "platform"],
)
async def test_websocket_get_trigger_capabilities(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
fake_integration,
trigger_key: str,
) -> None:
"""Test we get the expected trigger capabilities through websocket."""
await async_setup_component(hass, "device_automation", {})
@ -767,11 +772,12 @@ async def test_websocket_get_trigger_capabilities(
assert msg["id"] == 1
assert msg["type"] == TYPE_RESULT
assert msg["success"]
triggers = msg["result"]
triggers: dict = msg["result"]
msg_id = 2
assert len(triggers) == 3 # toggled, turned_on, turned_off
for trigger in triggers:
trigger[trigger_key] = trigger.pop("platform")
await client.send_json(
{
"id": msg_id,

View file

@ -1841,7 +1841,7 @@ async def test_nested_trigger_list() -> None:
"event_type": "trigger_3",
},
{
"platform": "event",
"trigger": "event",
"event_type": "trigger_4",
},
],
@ -1891,7 +1891,36 @@ async def test_nested_trigger_list_extra() -> None:
validated_triggers = TRIGGER_SCHEMA(trigger_config)
assert validated_triggers == trigger_config
assert validated_triggers == [
{
"platform": "other",
"triggers": [
{
"platform": "event",
"event_type": "trigger_1",
},
{
"platform": "event",
"event_type": "trigger_2",
},
],
},
]
async def test_trigger_backwards_compatibility() -> None:
"""Test triggers with backwards compatibility."""
assert cv._backward_compat_trigger_schema("str") == "str"
assert cv._backward_compat_trigger_schema({"platform": "abc"}) == {
"platform": "abc"
}
assert cv._backward_compat_trigger_schema({"trigger": "abc"}) == {"platform": "abc"}
with pytest.raises(
vol.Invalid,
match="Cannot specify both 'platform' and 'trigger'. Please use 'trigger' only.",
):
cv._backward_compat_trigger_schema({"trigger": "abc", "platform": "def"})
async def test_is_entity_service_schema(

View file

@ -1,6 +1,7 @@
"""Test selectors."""
from enum import Enum
from typing import Any
import pytest
import voluptuous as vol
@ -1107,6 +1108,13 @@ def test_condition_selector_schema(
(
{},
(
[
{
"platform": "numeric_state",
"entity_id": ["sensor.temperature"],
"below": 20,
}
],
[
{
"platform": "numeric_state",
@ -1122,7 +1130,24 @@ def test_condition_selector_schema(
)
def test_trigger_selector_schema(schema, valid_selections, invalid_selections) -> None:
"""Test trigger sequence selector."""
_test_selector("trigger", schema, valid_selections, invalid_selections)
def _custom_trigger_serializer(
triggers: list[dict[str, Any]],
) -> list[dict[str, Any]]:
res = []
for trigger in triggers:
if "trigger" in trigger:
trigger["platform"] = trigger.pop("trigger")
res.append(trigger)
return res
_test_selector(
"trigger",
schema,
valid_selections,
invalid_selections,
_custom_trigger_serializer,
)
@pytest.mark.parametrize(

View file

@ -11,7 +11,7 @@ blueprint:
number:
mode: "box"
triggers:
platform: event
trigger: event
event_type: !input trigger_event
actions:
service: !input service_to_call