Ejemplo n.º 1
0
def iter_components(config):
    for domain, conf in config.items():
        component = get_component(domain)
        if component.multi_conf:
            for conf_ in conf:
                yield domain, component, conf_
        else:
            yield domain, component, conf
        if component.is_platform_component:
            for p_config in conf:
                p_name = "{}.{}".format(domain, p_config[CONF_PLATFORM])
                platform = get_platform(domain, p_config[CONF_PLATFORM])
                yield p_name, platform, p_config
Ejemplo n.º 2
0
    def run(self, result: Config) -> None:
        if self.domain.startswith("."):
            # Ignore top-level keys starting with a dot
            return
        result.add_output_path([self.domain], self.domain)
        result[self.domain] = self.conf
        component = get_component(self.domain)
        path = [self.domain]
        if component is None:
            result.add_str_error(f"Component not found: {self.domain}", path)
            return
        CORE.loaded_integrations.add(self.domain)

        # Process AUTO_LOAD
        for load in component.auto_load:
            if load not in result:
                result.add_validation_step(AutoLoadValidationStep(load))

        if not component.is_platform_component:
            result.add_validation_step(
                MetadataValidationStep([self.domain], self.domain, self.conf,
                                       component))
            return

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

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

        for i, p_config in enumerate(self.conf):
            path = [self.domain, i]
            # Construct temporary unknown output path
            p_domain = f"{self.domain}.unknown"
            result.add_output_path(path, p_domain)
            result[self.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"{self.domain}.{p_name}"
            result.add_output_path(path, p_domain)
            # Try Load platform
            platform = get_platform(self.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 result:
                    result.add_validation_step(AutoLoadValidationStep(load))

            result.add_validation_step(
                MetadataValidationStep(path, p_domain, p_config, platform))
Ejemplo n.º 3
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)
    return result
Ejemplo n.º 4
0
def build_schema():
    print("Building schema")

    # check esphome was not loaded globally (IDE auto imports)
    if len(ejs.extended_schemas) == 0:
        raise Exception(
            "no data collected. Did you globally import an ESPHome component?")

    # Core schema
    schema_core[S_SCHEMAS] = {}
    register_module_schemas("core", cv)

    platforms = {}
    schema_core[S_PLATFORMS] = platforms
    core_components = {}
    schema_core[S_COMPONENTS] = core_components

    add_pin_validators()

    # Load a preview of each component
    for domain, manifest in components.items():
        if manifest.is_platform_component:
            # e.g. sensor, binary sensor, add S_COMPONENTS
            # note: S_COMPONENTS is not filled until loaded, e.g.
            # if lock: is not used, then we don't need to know about their
            # platforms yet.
            output[domain] = {S_COMPONENTS: {}, S_SCHEMAS: {}}
            platforms[domain] = {}
        elif manifest.config_schema is not None:
            # e.g. dallas
            output[domain] = {S_SCHEMAS: {S_CONFIG_SCHEMA: {}}}

    # Generate platforms (e.g. sensor, binary_sensor, climate )
    for domain in platforms:
        c = components[domain]
        register_module_schemas(domain, c.module)

    # Generate components
    for domain, manifest in components.items():
        if domain not in platforms:
            if manifest.config_schema is not None:
                core_components[domain] = {}
            register_module_schemas(domain, manifest.module, manifest)

        for platform in platforms:
            platform_manifest = get_platform(domain=platform, platform=domain)
            if platform_manifest is not None:
                output[platform][S_COMPONENTS][domain] = {}
                register_module_schemas(f"{domain}.{platform}",
                                        platform_manifest.module)

    # Do registries
    add_module_registries("core", automation)
    for domain, manifest in components.items():
        add_module_registries(domain, manifest.module)
    add_module_registries("remote_base", remote_base)

    # update props pointing to registries
    for reg_config_var in solve_registry:
        (registry, config_var) = reg_config_var
        config_var[S_TYPE] = "registry"
        config_var["registry"] = found_registries[repr(registry)]

    do_pins()
    do_esp8266()
    do_esp32()
    fix_remote_receiver()
    shrink()

    # aggregate components, so all component info is in same file, otherwise we have dallas.json, dallas.sensor.json, etc.
    data = {}
    for component, component_schemas in output.items():
        if "." in component:
            key = component.partition(".")[0]
            if key not in data:
                data[key] = {}
            data[key][component] = component_schemas
        else:
            if component not in data:
                data[component] = {}
            data[component] |= {component: component_schemas}

    # bundle core inside esphome
    data["esphome"]["core"] = data.pop("core")["core"]

    for c, s in data.items():
        write_file(c, s)