diff --git a/homeassistant/const.py b/homeassistant/const.py index ee90ebfc28b..45d6a97885b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -282,6 +282,7 @@ CONF_THEN: Final = "then" CONF_TIMEOUT: Final = "timeout" CONF_TIME_ZONE: Final = "time_zone" CONF_TOKEN: Final = "token" +CONF_TRIGGERS: Final = "triggers" CONF_TRIGGER_TIME: Final = "trigger_time" CONF_TTL: Final = "ttl" CONF_TYPE: Final = "type" diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index d88c388f9c7..059be3026e5 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -81,6 +81,7 @@ from homeassistant.const import ( CONF_TARGET, CONF_THEN, CONF_TIMEOUT, + CONF_TRIGGERS, CONF_UNTIL, CONF_VALUE_TEMPLATE, CONF_VARIABLES, @@ -1781,6 +1782,19 @@ TRIGGER_BASE_SCHEMA = vol.Schema( _base_trigger_validator_schema = TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) +def _base_trigger_list_flatten(triggers: list[Any]) -> list[Any]: + """Flatten trigger arrays containing 'triggers:' sublists into a single list of triggers.""" + flatlist = [] + for t in triggers: + if CONF_TRIGGERS in t and len(t.keys()) == 1: + triggerlist = ensure_list(t[CONF_TRIGGERS]) + flatlist.extend(triggerlist) + else: + flatlist.append(t) + + return flatlist + + # This is first round of validation, we don't want to process the config here already, # just ensure basics as platform and ID are there. def _base_trigger_validator(value: Any) -> Any: @@ -1788,7 +1802,9 @@ def _base_trigger_validator(value: Any) -> Any: return value -TRIGGER_SCHEMA = vol.All(ensure_list, [_base_trigger_validator]) +TRIGGER_SCHEMA = vol.All( + ensure_list, _base_trigger_list_flatten, [_base_trigger_validator] +) _SCRIPT_DELAY_SCHEMA = vol.Schema( { diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 1608a856de8..0eae0c88581 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -25,6 +25,7 @@ from homeassistant.helpers import ( selector, template, ) +from homeassistant.helpers.config_validation import TRIGGER_SCHEMA def test_boolean() -> None: @@ -1817,6 +1818,82 @@ async def test_async_validate(hass: HomeAssistant, tmpdir: py.path.local) -> Non validator_calls = {} +async def test_nested_trigger_list() -> None: + """Test triggers within nested lists are flattened.""" + + trigger_config = [ + { + "triggers": { + "platform": "event", + "event_type": "trigger_1", + }, + }, + { + "platform": "event", + "event_type": "trigger_2", + }, + {"triggers": []}, + {"triggers": None}, + { + "triggers": [ + { + "platform": "event", + "event_type": "trigger_3", + }, + { + "platform": "event", + "event_type": "trigger_4", + }, + ], + }, + ] + + validated_triggers = TRIGGER_SCHEMA(trigger_config) + + assert validated_triggers == [ + { + "platform": "event", + "event_type": "trigger_1", + }, + { + "platform": "event", + "event_type": "trigger_2", + }, + { + "platform": "event", + "event_type": "trigger_3", + }, + { + "platform": "event", + "event_type": "trigger_4", + }, + ] + + +async def test_nested_trigger_list_extra() -> None: + """Test triggers key with extra keys is not modified.""" + + trigger_config = [ + { + "platform": "other", + "triggers": [ + { + "platform": "event", + "event_type": "trigger_1", + }, + { + "platform": "event", + "event_type": "trigger_2", + }, + ], + }, + ] + + validated_triggers = TRIGGER_SCHEMA(trigger_config) + + assert validated_triggers == trigger_config + + async def test_is_entity_service_schema( hass: HomeAssistant, ) -> None: diff --git a/tests/helpers/test_trigger.py b/tests/helpers/test_trigger.py index 0bd5da0707c..4fde2d0ee0a 100644 --- a/tests/helpers/test_trigger.py +++ b/tests/helpers/test_trigger.py @@ -159,6 +159,70 @@ async def test_trigger_enabled_templates( assert len(service_calls) == 2 +async def test_nested_trigger_list( + hass: HomeAssistant, service_calls: list[ServiceCall] +) -> None: + """Test triggers within nested list.""" + + assert await async_setup_component( + hass, + "automation", + { + "automation": { + "trigger": [ + { + "triggers": { + "platform": "event", + "event_type": "trigger_1", + }, + }, + { + "platform": "event", + "event_type": "trigger_2", + }, + {"triggers": []}, + {"triggers": None}, + { + "triggers": [ + { + "platform": "event", + "event_type": "trigger_3", + }, + { + "platform": "event", + "event_type": "trigger_4", + }, + ], + }, + ], + "action": { + "service": "test.automation", + }, + } + }, + ) + + hass.bus.async_fire("trigger_1") + await hass.async_block_till_done() + assert len(service_calls) == 1 + + hass.bus.async_fire("trigger_2") + await hass.async_block_till_done() + assert len(service_calls) == 2 + + hass.bus.async_fire("trigger_none") + await hass.async_block_till_done() + assert len(service_calls) == 2 + + hass.bus.async_fire("trigger_3") + await hass.async_block_till_done() + assert len(service_calls) == 3 + + hass.bus.async_fire("trigger_4") + await hass.async_block_till_done() + assert len(service_calls) == 4 + + async def test_trigger_enabled_template_limited( hass: HomeAssistant, service_calls: list[ServiceCall],