diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index fb7cd7c51b..ea79054c88 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -278,7 +278,18 @@ def _push_context( """Resolve a variable, recursively resolving any dependencies it references.""" value = unresolved_vars.pop(key, Missing) if value is Missing: - return Missing + # Either already resolved (in resolved_vars) or currently being + # resolved (self-reference from inside a dict-valued substitution). + # Returning what we have lets sibling references inside a dict + # value, e.g. ``${device.manufacturer}`` inside ``device.name``, + # see literal sibling values during their own resolution. + return resolved_vars.get(key, Missing) + if isinstance(value, dict): + # Dict-valued substitutions form a namespace; eagerly publish the + # original mapping so its members can reference each other while + # the dict's own substitution pass is still running. The entry is + # replaced with the fully-substituted dict once recursion returns. + resolved_vars[key] = value try: value = substitute(value, [], resolver_context, True) except UndefinedError as err: diff --git a/tests/unit_tests/fixtures/substitutions/18-dict_self_reference.approved.yaml b/tests/unit_tests/fixtures/substitutions/18-dict_self_reference.approved.yaml new file mode 100644 index 0000000000..e5e6d4568e --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/18-dict_self_reference.approved.yaml @@ -0,0 +1,16 @@ +substitutions: + device: + manufacturer: espressif + model: esp32 + mac_suffix: ffffff + name: espressif-esp32-ffffff + network: + host: example.com + port: 8080 + url: http://example.com:8080/api +esphome: + name: espressif-esp32-ffffff +test_list: + - espressif-esp32-ffffff + - http://example.com:8080/api + - espressif/esp32 diff --git a/tests/unit_tests/fixtures/substitutions/18-dict_self_reference.input.yaml b/tests/unit_tests/fixtures/substitutions/18-dict_self_reference.input.yaml new file mode 100644 index 0000000000..b27c4b8c29 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/18-dict_self_reference.input.yaml @@ -0,0 +1,18 @@ +substitutions: + device: + manufacturer: "espressif" + model: "esp32" + mac_suffix: "ffffff" + name: ${device.manufacturer}-${device.model}-${device.mac_suffix} + network: + host: "example.com" + port: 8080 + url: "http://${network.host}:${network.port}/api" + +esphome: + name: ${device.name} + +test_list: + - ${device.name} + - ${network.url} + - "${device.manufacturer}/${device.model}"