diff --git a/automation_inspector/app/references.py b/automation_inspector/app/references.py index db7603c..b355542 100644 --- a/automation_inspector/app/references.py +++ b/automation_inspector/app/references.py @@ -27,6 +27,25 @@ "humidity_entity_id", } +JINJA_CONTEXT_ROOTS = frozenset( + { + "config", + "context", + "item", + "loop", + "now", + "repeat", + "state", + "states", + "this", + "trigger", + "utcnow", + "value_json", + "variables", + "wait", + } +) + STANDARD_DOMAINS = frozenset( { "air_quality", @@ -197,10 +216,15 @@ def scan_string(value: str, key: str | None) -> None: template_matches = set(TEMPLATE_ENTITY_RE.findall(value)) for entity_id in template_matches: add(entity_id, "template") - explicit_key = key in ENTITY_VALUE_KEYS - source = "template" if "{{" in value or "{%" in value else "configuration" + templated = _is_template(value) + # A templated value is resolved by Home Assistant at runtime, so an + # entity-shaped key cannot vouch for the dotted tokens it contains. + explicit_key = key in ENTITY_VALUE_KEYS and not templated + source = "template" if templated else "configuration" for entity_id in ENTITY_ID_RE.findall(value): domain = entity_id.split(".", 1)[0] + if templated and domain in JINJA_CONTEXT_ROOTS: + continue if explicit_key or entity_id in template_matches or domain in domains: add(entity_id, "explicit" if explicit_key else source) diff --git a/tests/test_references.py b/tests/test_references.py index 868c93e..aaecab4 100644 --- a/tests/test_references.py +++ b/tests/test_references.py @@ -103,3 +103,64 @@ def test_ignores_entity_like_values_in_description() -> None: references = collect_entity_references(config, set()) assert references == {"sensor.real_dependency": {"explicit"}} + + +def test_ignores_jinja_context_paths_but_keeps_literal_template_entities() -> None: + config = { + "conditions": [ + { + "condition": "template", + "value_template": "{{ is_state('sensor.real_temperature', '20') }}", + } + ], + "actions": [ + { + "repeat": { + "for_each": [{"boolean": "input_boolean.window_pause"}], + "sequence": [ + { + "action": "input_boolean.turn_off", + "target": {"entity_id": "{{ repeat.item.boolean }}"}, + }, + { + "action": "logbook.log", + "data": {"message": "Triggered by {{ trigger.entity_id }}"}, + }, + ], + } + } + ], + } + + references = collect_entity_references(config, set()) + + assert references == { + "input_boolean.window_pause": {"configuration"}, + "sensor.real_temperature": {"template"}, + } + + +def test_keeps_template_entity_references_not_matched_by_helper_functions() -> None: + config = { + "conditions": [ + {"condition": "template", "value_template": "{{ has_value('sensor.alpha') }}"}, + {"condition": "template", "value_template": "{{ states.sensor.beta }}"}, + { + "condition": "template", + "value_template": ( + "{% if is_state('binary_sensor.door','on') " + "and has_value('sensor.temp') %}on{% endif %}" + ), + }, + ], + "actions": [], + } + + references = collect_entity_references(config, {"sensor", "binary_sensor"}) + + assert sorted(references) == [ + "binary_sensor.door", + "sensor.alpha", + "sensor.beta", + "sensor.temp", + ]