From 2f892b97ce7e44be8de0927d41796df4f04de750 Mon Sep 17 00:00:00 2001 From: Nate Clark Date: Tue, 18 Feb 2025 18:50:32 -0500 Subject: [PATCH] Add support for template variables in remote packages from Github --- esphome/components/packages/__init__.py | 13 +++-- esphome/const.py | 1 + esphome/yaml_util.py | 65 +++++++++++++------------ 3 files changed, 44 insertions(+), 35 deletions(-) diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 2b064a90cf..9f7aea0101 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -1,8 +1,8 @@ from pathlib import Path -import esphome.config_validation as cv from esphome import git, yaml_util from esphome.config_helpers import merge_config +import esphome.config_validation as cv from esphome.const import ( CONF_ESPHOME, CONF_FILE, @@ -14,8 +14,9 @@ from esphome.const import ( CONF_REFRESH, CONF_URL, CONF_USERNAME, + CONF_VARS, + __version__ as ESPHOME_VERSION, ) -from esphome.const import __version__ as ESPHOME_VERSION from esphome.core import EsphomeError DOMAIN = CONF_PACKAGES @@ -81,6 +82,7 @@ BASE_SCHEMA = cv.All( cv.Optional(CONF_REFRESH, default="1d"): cv.All( cv.string, cv.source_refresh ), + cv.Optional(CONF_VARS): dict, } ), cv.has_at_least_one_key(CONF_FILE, CONF_FILES), @@ -107,6 +109,11 @@ def _process_base_package(config: dict) -> dict: password=config.get(CONF_PASSWORD), ) files: list[str] = config[CONF_FILES] + vars: dict = config.get(CONF_VARS) + if vars: + vars = {k: str(v) for k, v in vars.items()} + else: + vars = {} def get_packages(files) -> dict: packages = {} @@ -132,7 +139,7 @@ def _process_base_package(config: dict) -> dict: f"Current ESPHome Version is too old to use this package: {ESPHOME_VERSION} < {min_version}" ) - packages[file] = new_yaml + packages[file] = yaml_util.substitute_vars(new_yaml, vars) except EsphomeError as e: raise cv.Invalid( f"{file} is not a valid YAML file. Please check the file contents.\n{e}" diff --git a/esphome/const.py b/esphome/const.py index f74ea64148..a0e0bea0c8 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -927,6 +927,7 @@ CONF_VALUE = "value" CONF_VALUE_FONT = "value_font" CONF_VARIABLES = "variables" CONF_VARIANT = "variant" +CONF_VARS = "vars" CONF_VERSION = "version" CONF_VIBRATIONS = "vibrations" CONF_VISIBLE = "visible" diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index b27ce4c3e3..71e5d423bf 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -283,38 +283,6 @@ class ESPHomeLoaderMixin: vars = {k: str(v) for k, v in vars.items()} return file, vars - def substitute_vars(config, vars): - from esphome.components import substitutions - from esphome.const import CONF_DEFAULTS, CONF_SUBSTITUTIONS - - org_subs = None - result = config - if not isinstance(config, dict): - # when the included yaml contains a list or a scalar - # wrap it into an OrderedDict because do_substitution_pass expects it - result = OrderedDict([("yaml", config)]) - elif CONF_SUBSTITUTIONS in result: - org_subs = result.pop(CONF_SUBSTITUTIONS) - - defaults = {} - if CONF_DEFAULTS in result: - defaults = result.pop(CONF_DEFAULTS) - - result[CONF_SUBSTITUTIONS] = vars - for k, v in defaults.items(): - if k not in result[CONF_SUBSTITUTIONS]: - result[CONF_SUBSTITUTIONS][k] = v - - # Ignore missing vars that refer to the top level substitutions - substitutions.do_substitution_pass(result, None, ignore_missing=True) - result.pop(CONF_SUBSTITUTIONS) - - if not isinstance(config, dict): - result = result["yaml"] # unwrap the result - elif org_subs: - result[CONF_SUBSTITUTIONS] = org_subs - return result - if isinstance(node, yaml.nodes.MappingNode): file, vars = extract_file_vars(node) else: @@ -482,6 +450,39 @@ def _find_files(directory, pattern): yield filename +def substitute_vars(config, vars): + from esphome.components import substitutions + from esphome.const import CONF_DEFAULTS, CONF_SUBSTITUTIONS + + org_subs = None + result = config + if not isinstance(config, dict): + # when the included yaml contains a list or a scalar + # wrap it into an OrderedDict because do_substitution_pass expects it + result = OrderedDict([("yaml", config)]) + elif CONF_SUBSTITUTIONS in result: + org_subs = result.pop(CONF_SUBSTITUTIONS) + + defaults = {} + if CONF_DEFAULTS in result: + defaults = result.pop(CONF_DEFAULTS) + + result[CONF_SUBSTITUTIONS] = vars + for k, v in defaults.items(): + if k not in result[CONF_SUBSTITUTIONS]: + result[CONF_SUBSTITUTIONS][k] = v + + # Ignore missing vars that refer to the top level substitutions + substitutions.do_substitution_pass(result, None, ignore_missing=True) + result.pop(CONF_SUBSTITUTIONS) + + if not isinstance(config, dict): + result = result["yaml"] # unwrap the result + elif org_subs: + result[CONF_SUBSTITUTIONS] = org_subs + return result + + def is_secret(value): try: return _SECRET_VALUES[str(value)]