Skip to content

Commit ff40ce4

Browse files
authored
Add context support for triggers.yaml (home-assistant#156531)
1 parent d953087 commit ff40ce4

File tree

2 files changed

+102
-5
lines changed

2 files changed

+102
-5
lines changed

homeassistant/helpers/selector.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ class Selector[_T: Mapping[str, Any]]:
5757
CONFIG_SCHEMA: Callable
5858
config: _T
5959
selector_type: str
60+
# Context keys that are allowed to be used in the selector, with list of allowed selector types.
61+
# Selectors can use the value of other fields in the same schema as context for filtering for example.
62+
# The selector defines which context keys it supports and what selector types are allowed for each key.
63+
allowed_context_keys: dict[str, set[str]] = {}
6064

6165
def __init__(self, config: Mapping[str, Any] | None = None) -> None:
6266
"""Instantiate a selector."""
@@ -346,6 +350,11 @@ class AttributeSelector(Selector[AttributeSelectorConfig]):
346350

347351
selector_type = "attribute"
348352

353+
allowed_context_keys = {
354+
# Filters the available attributes based on the selected entity
355+
"filter_entity": {"entity"}
356+
}
357+
349358
CONFIG_SCHEMA = make_selector_config_schema(
350359
{
351360
vol.Required("entity_id"): cv.entity_id,
@@ -1039,6 +1048,11 @@ class MediaSelector(Selector[MediaSelectorConfig]):
10391048

10401049
selector_type = "media"
10411050

1051+
allowed_context_keys = {
1052+
# Filters the available media based on the selected entity
1053+
"filter_entity": {EntitySelector.selector_type}
1054+
}
1055+
10421056
CONFIG_SCHEMA = make_selector_config_schema(
10431057
{
10441058
vol.Optional("accept"): [str],
@@ -1385,6 +1399,15 @@ class StateSelector(Selector[StateSelectorConfig]):
13851399

13861400
selector_type = "state"
13871401

1402+
allowed_context_keys = {
1403+
# Filters the available states based on the selected entity
1404+
"filter_entity": {EntitySelector.selector_type},
1405+
# Filters the available states based on the selected target
1406+
"filter_target": {"target"},
1407+
# Only show the attribute values of a specific attribute
1408+
"filter_attribute": {AttributeSelector.selector_type},
1409+
}
1410+
13881411
CONFIG_SCHEMA = make_selector_config_schema(
13891412
{
13901413
vol.Optional("entity_id"): cv.entity_id,

script/hassfest/triggers.py

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,95 @@ def exists(value: Any) -> Any:
2626
return value
2727

2828

29+
def validate_field_schema(trigger_schema: dict[str, Any]) -> dict[str, Any]:
30+
"""Validate a field schema including context references."""
31+
32+
for field_name, field_schema in trigger_schema.get("fields", {}).items():
33+
# Validate context if present
34+
if "context" in field_schema:
35+
if CONF_SELECTOR not in field_schema:
36+
raise vol.Invalid(
37+
f"Context defined without a selector in '{field_name}'"
38+
)
39+
40+
context = field_schema["context"]
41+
if not isinstance(context, dict):
42+
raise vol.Invalid(f"Context must be a dictionary in '{field_name}'")
43+
44+
# Determine which selector type is being used
45+
selector_config = field_schema[CONF_SELECTOR]
46+
selector_class = selector.selector(selector_config)
47+
48+
for context_key, field_ref in context.items():
49+
# Check if context key is allowed for this selector type
50+
allowed_keys = selector_class.allowed_context_keys
51+
if context_key not in allowed_keys:
52+
raise vol.Invalid(
53+
f"Invalid context key '{context_key}' for selector type '{selector_class.selector_type}'. "
54+
f"Allowed keys: {', '.join(sorted(allowed_keys)) if allowed_keys else 'none'}"
55+
)
56+
57+
# Check if the referenced field exists in trigger schema or target
58+
if not isinstance(field_ref, str):
59+
raise vol.Invalid(
60+
f"Context value for '{context_key}' must be a string field reference"
61+
)
62+
63+
# Check if field exists in trigger schema fields or target
64+
trigger_fields = trigger_schema["fields"]
65+
field_exists = field_ref in trigger_fields
66+
if field_exists and "selector" in trigger_fields[field_ref]:
67+
# Check if the selector type is allowed for this context key
68+
field_selector_config = trigger_fields[field_ref][CONF_SELECTOR]
69+
field_selector_class = selector.selector(field_selector_config)
70+
if field_selector_class.selector_type not in allowed_keys.get(
71+
context_key, set()
72+
):
73+
raise vol.Invalid(
74+
f"The context '{context_key}' for '{field_name}' references '{field_ref}', but '{context_key}' "
75+
f"does not allow selectors of type '{field_selector_class.selector_type}'. Allowed selector types: {', '.join(allowed_keys.get(context_key, set()))}"
76+
)
77+
if not field_exists and "target" in trigger_schema:
78+
# Target is a special field that always exists when defined
79+
field_exists = field_ref == "target"
80+
if field_exists and "target" not in allowed_keys.get(
81+
context_key, set()
82+
):
83+
raise vol.Invalid(
84+
f"The context '{context_key}' for '{field_name}' references 'target', but '{context_key}' "
85+
f"does not allow 'target'. Allowed selector types: {', '.join(allowed_keys.get(context_key, set()))}"
86+
)
87+
88+
if not field_exists:
89+
raise vol.Invalid(
90+
f"Context reference '{field_ref}' for key '{context_key}' does not exist "
91+
f"in trigger schema fields or target"
92+
)
93+
94+
return trigger_schema
95+
96+
2997
FIELD_SCHEMA = vol.Schema(
3098
{
3199
vol.Optional("example"): exists,
32100
vol.Optional("default"): exists,
33101
vol.Optional("required"): bool,
34102
vol.Optional(CONF_SELECTOR): selector.validate_selector,
103+
vol.Optional("context"): {
104+
str: str # key is context key, value is field name in the schema which value should be used
105+
}, # Will be validated in validate_field_schema
35106
}
36107
)
37108

38109
TRIGGER_SCHEMA = vol.Any(
39-
vol.Schema(
40-
{
41-
vol.Optional("target"): selector.TargetSelector.CONFIG_SCHEMA,
42-
vol.Optional("fields"): vol.Schema({str: FIELD_SCHEMA}),
43-
}
110+
vol.All(
111+
vol.Schema(
112+
{
113+
vol.Optional("target"): selector.TargetSelector.CONFIG_SCHEMA,
114+
vol.Optional("fields"): vol.Schema({str: FIELD_SCHEMA}),
115+
}
116+
),
117+
validate_field_schema,
44118
),
45119
None,
46120
)

0 commit comments

Comments
 (0)