Esempio n. 1
0
def test_include_with_vars(fixture_path):
    yaml_file = fixture_path / "yaml_util" / "includetest.yaml"

    actual = yaml_util.load_yaml(yaml_file)
    substitutions.do_substitution_pass(actual, None)
    assert actual["esphome"]["name"] == "original"
    assert actual["esphome"]["libraries"][0] == "Wire"
    assert actual["esphome"]["board"] == "nodemcu"
    assert actual["wifi"]["ssid"] == "my_custom_ssid"
Esempio n. 2
0
        def substitute_vars(config, vars):
            from esphome.const import CONF_SUBSTITUTIONS
            from esphome.components import 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)

            result[CONF_SUBSTITUTIONS] = vars
            # 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
Esempio n. 3
0
def load_config():
    try:
        config = yaml_util.load_yaml(CORE.config_path)
    except OSError:
        raise EsphomeError(u"Invalid YAML at {}".format(CORE.config_path))
    CORE.raw_config = config
    config = substitutions.do_substitution_pass(config)
    core_config.preload_core_config(config)

    try:
        result = validate_config(config)
    except EsphomeError:
        raise
    except Exception:
        _LOGGER.error(u"Unexpected exception while reading configuration:")
        raise

    return result
Esempio n. 4
0
def validate_config(config):
    result = Config()

    # 1. Load substitutions
    if CONF_SUBSTITUTIONS in config:
        result[CONF_SUBSTITUTIONS] = config[CONF_SUBSTITUTIONS]
        result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS)
        try:
            substitutions.do_substitution_pass(config)
        except vol.Invalid as err:
            result.add_error(err)
            return result

    if 'esphomeyaml' in config:
        _LOGGER.warning(
            "The esphomeyaml section has been renamed to esphome in 1.11.0. "
            "Please replace 'esphomeyaml:' in your configuration with 'esphome:'."
        )
        config[CONF_ESPHOME] = config.pop('esphomeyaml')

    if CONF_ESPHOME not in config:
        result.add_str_error(
            "'esphome' section missing from configuration. Please make sure "
            "your configuration has an 'esphome:' line in it.", [])
        return result

    # 2. Load partial core config
    result[CONF_ESPHOME] = config[CONF_ESPHOME]
    result.add_output_path([CONF_ESPHOME], CONF_ESPHOME)
    try:
        core_config.preload_core_config(config)
    except vol.Invalid as err:
        result.add_error(err)
        return result
    # Remove temporary esphome config path again, it will be reloaded later
    result.remove_output_path([CONF_ESPHOME], CONF_ESPHOME)

    # 3. Load components.
    # Load components (also AUTO_LOAD) and set output paths of result
    # Queue of items to load, FIFO
    load_queue = collections.deque()
    for domain, conf in config.items():
        load_queue.append((domain, conf))

    # List of items to enter next stage
    check_queue = [
    ]  # type: List[Tuple[ConfigPath, str, ConfigType, ComponentManifest]]

    # This step handles:
    # - Adding output path
    # - Auto Load
    # - Loading configs into result

    while load_queue:
        domain, conf = load_queue.popleft()
        domain = text_type(domain)
        if domain.startswith(u'.'):
            # Ignore top-level keys starting with a dot
            continue
        result.add_output_path([domain], domain)
        result[domain] = conf
        component = get_component(domain)
        path = [domain]
        if component is None:
            result.add_str_error(u"Component not found: {}".format(domain),
                                 path)
            continue
        CORE.loaded_integrations.add(domain)

        # Process AUTO_LOAD
        for load in component.auto_load:
            if load not in config:
                load_conf = core.AutoLoad()
                config[load] = load_conf
                load_queue.append((load, load_conf))

        if not component.is_platform_component:
            check_queue.append(([domain], domain, conf, component))
            continue

        # This is a platform component, proceed to reading platform entries
        # Remove this is as an output path
        result.remove_output_path([domain], domain)

        # Ensure conf is a list
        if not conf:
            result[domain] = conf = []
        elif not isinstance(conf, list):
            result[domain] = conf = [conf]

        for i, p_config in enumerate(conf):
            path = [domain, i]
            # Construct temporary unknown output path
            p_domain = u'{}.unknown'.format(domain)
            result.add_output_path(path, p_domain)
            result[domain][i] = p_config
            if not isinstance(p_config, dict):
                result.add_str_error(
                    u"Platform schemas must be key-value pairs.", path)
                continue
            p_name = p_config.get('platform')
            if p_name is None:
                result.add_str_error(
                    u"No platform specified! See 'platform' key.", path)
                continue
            # Remove temp output path and construct new one
            result.remove_output_path(path, p_domain)
            p_domain = u'{}.{}'.format(domain, p_name)
            result.add_output_path(path, p_domain)
            # Try Load platform
            platform = get_platform(domain, p_name)
            if platform is None:
                result.add_str_error(
                    u"Platform not found: '{}'".format(p_domain), path)
                continue
            CORE.loaded_integrations.add(p_name)

            # Process AUTO_LOAD
            for load in platform.auto_load:
                if load not in config:
                    load_conf = core.AutoLoad()
                    config[load] = load_conf
                    load_queue.append((load, load_conf))

            check_queue.append((path, p_domain, p_config, platform))

    # 4. Validate component metadata, including
    # - Transformation (nullable, multi conf)
    # - Dependencies
    # - Conflicts
    # - Supported ESP Platform

    # List of items to proceed to next stage
    validate_queue = [
    ]  # type: List[Tuple[ConfigPath, ConfigType, ComponentManifest]]
    for path, domain, conf, comp in check_queue:
        if conf is None:
            result[domain] = conf = {}

        success = True
        for dependency in comp.dependencies:
            if dependency not in config:
                result.add_str_error(
                    u"Component {} requires component {}"
                    u"".format(domain, dependency), path)
                success = False
        if not success:
            continue

        success = True
        for conflict in comp.conflicts_with:
            if conflict in config:
                result.add_str_error(
                    u"Component {} cannot be used together with component {}"
                    u"".format(domain, conflict), path)
                success = False
        if not success:
            continue

        if CORE.esp_platform not in comp.esp_platforms:
            result.add_str_error(
                u"Component {} doesn't support {}.".format(
                    domain, CORE.esp_platform), path)
            continue

        if not comp.is_platform_component and comp.config_schema is None and \
                not isinstance(conf, core.AutoLoad):
            result.add_str_error(
                u"Component {} cannot be loaded via YAML "
                u"(no CONFIG_SCHEMA).".format(domain), path)
            continue

        if comp.is_multi_conf:
            if not isinstance(conf, list):
                result[domain] = conf = [conf]
            for i, part_conf in enumerate(conf):
                validate_queue.append((path + [i], part_conf, comp))
            continue

        validate_queue.append((path, conf, comp))

    # 5. Validate configuration schema
    for path, conf, comp in validate_queue:
        if comp.config_schema is None:
            continue
        with result.catch_error(path):
            if comp.is_platform:
                # Remove 'platform' key for validation
                input_conf = OrderedDict(conf)
                platform_val = input_conf.pop('platform')
                validated = comp.config_schema(input_conf)
                # Ensure result is OrderedDict so we can call move_to_end
                if not isinstance(validated, OrderedDict):
                    validated = OrderedDict(validated)
                validated['platform'] = platform_val
                validated.move_to_end('platform', last=False)
                result.set_by_path(path, validated)
            else:
                validated = comp.config_schema(conf)
                result.set_by_path(path, validated)

    # 6. If no validation errors, check IDs
    if not result.errors:
        # Only parse IDs if no validation error. Otherwise
        # user gets confusing messages
        do_id_pass(result)
    return result
Esempio n. 5
0
def validate_config(config, command_line_substitutions):
    result = Config()

    loader.clear_component_meta_finders()
    loader.install_custom_components_meta_finder()

    # 0. Load packages
    if CONF_PACKAGES in config:
        from esphome.components.packages import do_packages_pass

        result.add_output_path([CONF_PACKAGES], CONF_PACKAGES)
        try:
            config = do_packages_pass(config)
        except vol.Invalid as err:
            result.update(config)
            result.add_error(err)
            return result

    CORE.raw_config = config

    # 1. Load substitutions
    if CONF_SUBSTITUTIONS in config:
        from esphome.components import substitutions

        result[CONF_SUBSTITUTIONS] = {
            **config[CONF_SUBSTITUTIONS],
            **command_line_substitutions,
        }
        result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS)
        try:
            substitutions.do_substitution_pass(config,
                                               command_line_substitutions)
        except vol.Invalid as err:
            result.add_error(err)
            return result

    CORE.raw_config = config

    # 1.1. Check for REPLACEME special value
    try:
        recursive_check_replaceme(config)
    except vol.Invalid as err:
        result.add_error(err)

    # 1.2. Load external_components
    if CONF_EXTERNAL_COMPONENTS in config:
        from esphome.components.external_components import do_external_components_pass

        result.add_output_path([CONF_EXTERNAL_COMPONENTS],
                               CONF_EXTERNAL_COMPONENTS)
        try:
            do_external_components_pass(config)
        except vol.Invalid as err:
            result.update(config)
            result.add_error(err)
            return result

    if "esphomeyaml" in config:
        _LOGGER.warning(
            "The esphomeyaml section has been renamed to esphome in 1.11.0. "
            "Please replace 'esphomeyaml:' in your configuration with 'esphome:'."
        )
        config[CONF_ESPHOME] = config.pop("esphomeyaml")

    if CONF_ESPHOME not in config:
        result.add_str_error(
            "'esphome' section missing from configuration. Please make sure "
            "your configuration has an 'esphome:' line in it.",
            [],
        )
        return result

    # 2. Load partial core config
    result[CONF_ESPHOME] = config[CONF_ESPHOME]
    result.add_output_path([CONF_ESPHOME], CONF_ESPHOME)
    try:
        core_config.preload_core_config(config, result)
    except vol.Invalid as err:
        result.add_error(err)
        return result
    # Remove temporary esphome config path again, it will be reloaded later
    result.remove_output_path([CONF_ESPHOME], CONF_ESPHOME)

    # First run platform validation steps
    for key in TARGET_PLATFORMS:
        if key in config:
            result.add_validation_step(LoadValidationStep(key, config[key]))
    result.run_validation_steps()

    if result.errors:
        return result

    for domain, conf in config.items():
        result.add_validation_step(LoadValidationStep(domain, conf))
    result.add_validation_step(IDPassValidationStep())

    result.run_validation_steps()

    return result
Esempio n. 6
0
def validate_config(config, command_line_substitutions):
    result = Config()

    loader.clear_component_meta_finders()
    loader.install_custom_components_meta_finder()

    # 0. Load packages
    if CONF_PACKAGES in config:
        from esphome.components.packages import do_packages_pass

        result.add_output_path([CONF_PACKAGES], CONF_PACKAGES)
        try:
            config = do_packages_pass(config)
        except vol.Invalid as err:
            result.update(config)
            result.add_error(err)
            return result

    # 1. Load substitutions
    if CONF_SUBSTITUTIONS in config:
        from esphome.components import substitutions

        result[CONF_SUBSTITUTIONS] = {
            **config[CONF_SUBSTITUTIONS],
            **command_line_substitutions,
        }
        result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS)
        try:
            substitutions.do_substitution_pass(config,
                                               command_line_substitutions)
        except vol.Invalid as err:
            result.add_error(err)
            return result

    # 1.1. Check for REPLACEME special value
    try:
        recursive_check_replaceme(config)
    except vol.Invalid as err:
        result.add_error(err)

    # 1.2. Load external_components
    if CONF_EXTERNAL_COMPONENTS in config:
        from esphome.components.external_components import do_external_components_pass

        result.add_output_path([CONF_EXTERNAL_COMPONENTS],
                               CONF_EXTERNAL_COMPONENTS)
        try:
            do_external_components_pass(config)
        except vol.Invalid as err:
            result.update(config)
            result.add_error(err)
            return result

    if "esphomeyaml" in config:
        _LOGGER.warning(
            "The esphomeyaml section has been renamed to esphome in 1.11.0. "
            "Please replace 'esphomeyaml:' in your configuration with 'esphome:'."
        )
        config[CONF_ESPHOME] = config.pop("esphomeyaml")

    if CONF_ESPHOME not in config:
        result.add_str_error(
            "'esphome' section missing from configuration. Please make sure "
            "your configuration has an 'esphome:' line in it.",
            [],
        )
        return result

    # 2. Load partial core config
    result[CONF_ESPHOME] = config[CONF_ESPHOME]
    result.add_output_path([CONF_ESPHOME], CONF_ESPHOME)
    try:
        core_config.preload_core_config(config)
    except vol.Invalid as err:
        result.add_error(err)
        return result
    # Remove temporary esphome config path again, it will be reloaded later
    result.remove_output_path([CONF_ESPHOME], CONF_ESPHOME)

    # 3. Load components.
    # Load components (also AUTO_LOAD) and set output paths of result
    # Queue of items to load, FIFO
    load_queue = collections.deque()
    for domain, conf in config.items():
        load_queue.append((domain, conf))

    # List of items to enter next stage
    check_queue = (
        []
    )  # type: List[Tuple[ConfigPath, str, ConfigType, ComponentManifest]]

    # This step handles:
    # - Adding output path
    # - Auto Load
    # - Loading configs into result

    while load_queue:
        domain, conf = load_queue.popleft()
        if domain.startswith("."):
            # Ignore top-level keys starting with a dot
            continue
        result.add_output_path([domain], domain)
        result[domain] = conf
        component = get_component(domain)
        path = [domain]
        if component is None:
            result.add_str_error(f"Component not found: {domain}", path)
            continue
        CORE.loaded_integrations.add(domain)

        # Process AUTO_LOAD
        for load in component.auto_load:
            if load not in config:
                load_conf = core.AutoLoad()
                config[load] = load_conf
                load_queue.append((load, load_conf))

        if not component.is_platform_component:
            check_queue.append(([domain], domain, conf, component))
            continue

        # This is a platform component, proceed to reading platform entries
        # Remove this is as an output path
        result.remove_output_path([domain], domain)

        # Ensure conf is a list
        if not conf:
            result[domain] = conf = []
        elif not isinstance(conf, list):
            result[domain] = conf = [conf]

        for i, p_config in enumerate(conf):
            path = [domain, i]
            # Construct temporary unknown output path
            p_domain = f"{domain}.unknown"
            result.add_output_path(path, p_domain)
            result[domain][i] = p_config
            if not isinstance(p_config, dict):
                result.add_str_error(
                    "Platform schemas must be key-value pairs.", path)
                continue
            p_name = p_config.get("platform")
            if p_name is None:
                result.add_str_error(
                    "No platform specified! See 'platform' key.", path)
                continue
            # Remove temp output path and construct new one
            result.remove_output_path(path, p_domain)
            p_domain = f"{domain}.{p_name}"
            result.add_output_path(path, p_domain)
            # Try Load platform
            platform = get_platform(domain, p_name)
            if platform is None:
                result.add_str_error(f"Platform not found: '{p_domain}'", path)
                continue
            CORE.loaded_integrations.add(p_name)

            # Process AUTO_LOAD
            for load in platform.auto_load:
                if load not in config:
                    load_conf = core.AutoLoad()
                    config[load] = load_conf
                    load_queue.append((load, load_conf))

            check_queue.append((path, p_domain, p_config, platform))

    # 4. Validate component metadata, including
    # - Transformation (nullable, multi conf)
    # - Dependencies
    # - Conflicts
    # - Supported ESP Platform

    # List of items to proceed to next stage
    validate_queue = [
    ]  # type: List[Tuple[ConfigPath, ConfigType, ComponentManifest]]
    for path, domain, conf, comp in check_queue:
        if conf is None:
            result[domain] = conf = {}

        success = True
        for dependency in comp.dependencies:
            if dependency not in config:
                result.add_str_error(
                    "Component {} requires component {}"
                    "".format(domain, dependency),
                    path,
                )
                success = False
        if not success:
            continue

        success = True
        for conflict in comp.conflicts_with:
            if conflict in config:
                result.add_str_error(
                    "Component {} cannot be used together with component {}"
                    "".format(domain, conflict),
                    path,
                )
                success = False
        if not success:
            continue

        if CORE.esp_platform not in comp.esp_platforms:
            result.add_str_error(
                "Component {} doesn't support {}.".format(
                    domain, CORE.esp_platform),
                path,
            )
            continue

        if (not comp.is_platform_component and comp.config_schema is None
                and not isinstance(conf, core.AutoLoad)):
            result.add_str_error(
                "Component {} cannot be loaded via YAML "
                "(no CONFIG_SCHEMA).".format(domain),
                path,
            )
            continue

        if comp.multi_conf:
            if not isinstance(conf, list):
                result[domain] = conf = [conf]
            if not isinstance(comp.multi_conf,
                              bool) and len(conf) > comp.multi_conf:
                result.add_str_error(
                    "Component {} supports a maximum of {} "
                    "entries ({} found).".format(domain, comp.multi_conf,
                                                 len(conf)),
                    path,
                )
                continue
            for i, part_conf in enumerate(conf):
                validate_queue.append((path + [i], part_conf, comp))
            continue

        validate_queue.append((path, conf, comp))

    # 5. Validate configuration schema
    for path, conf, comp in validate_queue:
        if comp.config_schema is None:
            continue
        with result.catch_error(path):
            if comp.is_platform:
                # Remove 'platform' key for validation
                input_conf = OrderedDict(conf)
                platform_val = input_conf.pop("platform")
                validated = comp.config_schema(input_conf)
                # Ensure result is OrderedDict so we can call move_to_end
                if not isinstance(validated, OrderedDict):
                    validated = OrderedDict(validated)
                validated["platform"] = platform_val
                validated.move_to_end("platform", last=False)
                result.set_by_path(path, validated)
            else:
                validated = comp.config_schema(conf)
                result.set_by_path(path, validated)

    # 6. If no validation errors, check IDs
    if not result.errors:
        # Only parse IDs if no validation error. Otherwise
        # user gets confusing messages
        do_id_pass(result)

    # 7. Final validation
    if not result.errors:
        # Inter - components validation
        for path, conf, comp in validate_queue:
            if comp.config_schema is None:
                continue
            if callable(comp.validate):
                try:
                    comp.validate(result, result.get_nested_item(path))
                except ValueError as err:
                    result.add_str_error(err, path)

    return result