diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 2b064a90cf..730bc9d0ab 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -1,21 +1,23 @@ 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, CONF_FILES, CONF_MIN_VERSION, + CONF_NAME, CONF_PACKAGES, CONF_PASSWORD, CONF_REF, 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 @@ -74,7 +76,19 @@ BASE_SCHEMA = cv.All( cv.Optional(CONF_PASSWORD): cv.string, cv.Exclusive(CONF_FILE, "files"): validate_yaml_filename, cv.Exclusive(CONF_FILES, "files"): cv.All( - cv.ensure_list(validate_yaml_filename), + cv.ensure_list( + cv.Any( + validate_yaml_filename, + cv.Schema( + { + cv.Required(CONF_NAME): validate_yaml_filename, + cv.Optional(CONF_VARS, default={}): cv.Schema( + {cv.string: cv.string} + ), + } + ), + ) + ), cv.Length(min=1), ), cv.Optional(CONF_REF): cv.git_ref, @@ -106,16 +120,25 @@ def _process_base_package(config: dict) -> dict: username=config.get(CONF_USERNAME), password=config.get(CONF_PASSWORD), ) - files: list[str] = config[CONF_FILES] + files = [] + for file in config[CONF_FILES]: + if isinstance(file, str): + files.append({CONF_NAME: file, CONF_VARS: {}}) + else: + files.append(file) + print(files) def get_packages(files) -> dict: packages = {} - for file in files: - yaml_file: Path = repo_dir / file + for idx, file in enumerate(files): + filename = file[CONF_NAME] + yaml_file: Path = repo_dir / filename + vars = file.get(CONF_VARS) if not yaml_file.is_file(): raise cv.Invalid( - f"{file} does not exist in repository", path=[CONF_FILES] + f"{filename} does not exist in repository", + path=[CONF_FILES], ) try: @@ -131,11 +154,13 @@ def _process_base_package(config: dict) -> dict: raise cv.Invalid( f"Current ESPHome Version is too old to use this package: {ESPHOME_VERSION} < {min_version}" ) - - packages[file] = new_yaml + if vars: + vars = {k: str(v) for k, v in vars.items()} + new_yaml = yaml_util.substitute_vars(new_yaml, vars) + packages[f"{filename}{idx}"] = new_yaml except EsphomeError as e: raise cv.Invalid( - f"{file} is not a valid YAML file. Please check the file contents.\n{e}" + f"{filename} is not a valid YAML file. Please check the file contents.\n{e}" ) from e return packages diff --git a/esphome/const.py b/esphome/const.py index 0f41dc1aec..c90e13e7cc 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -930,6 +930,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 d67511dfec..fb341c59d2 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -273,48 +273,18 @@ class ESPHomeLoaderMixin: @_add_data_ref def construct_include(self, node): + from esphome.const import CONF_VARS + def extract_file_vars(node): fields = self.construct_yaml_map(node) file = fields.get("file") if file is None: raise yaml.MarkedYAMLError("Must include 'file'", node.start_mark) - vars = fields.get("vars") + vars = fields.get(CONF_VARS) if vars: 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: @@ -432,6 +402,39 @@ def parse_yaml(file_name: str, file_handle: TextIOWrapper) -> Any: ) +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 _load_yaml_internal(fname: str) -> Any: """Load a YAML file.""" try: