def as_dict(self): out = OrderedDict() if self.microseconds is not None: out["microseconds"] = self.microseconds if self.milliseconds is not None: out["milliseconds"] = self.milliseconds if self.seconds is not None: out["seconds"] = self.seconds if self.minutes is not None: out["minutes"] = self.minutes if self.hours is not None: out["hours"] = self.hours if self.days is not None: out["days"] = self.days return out
def as_dict(self): out = OrderedDict() if self.microseconds is not None: out['microseconds'] = self.microseconds if self.milliseconds is not None: out['milliseconds'] = self.milliseconds if self.seconds is not None: out['seconds'] = self.seconds if self.minutes is not None: out['minutes'] = self.minutes if self.hours is not None: out['hours'] = self.hours if self.days is not None: out['days'] = self.days return out
class StructInitializer(Expression): __slots__ = ("base", "args") def __init__(self, base: Expression, *args: Tuple[str, Optional[SafeExpType]]): self.base = base # TODO: args is always a Tuple, is this check required? if not isinstance(args, OrderedDict): args = OrderedDict(args) self.args = OrderedDict() for key, value in args.items(): if value is None: continue exp = safe_exp(value) self.args[key] = exp def __str__(self): cpp = f'{self.base}{{\n' for key, value in self.args.items(): cpp += f' .{key} = {value},\n' cpp += '}' return cpp
class StructInitializer(Expression): def __init__( self, base, *args): # type: (Expression, *Tuple[str, SafeExpType]) -> None super().__init__() self.base = base if not isinstance(args, OrderedDict): args = OrderedDict(args) self.args = OrderedDict() for key, value in args.items(): if value is None: continue exp = safe_exp(value) self.args[key] = exp def __str__(self): cpp = f'{self.base}{{\n' for key, value in self.args.items(): cpp += f' .{key} = {value},\n' cpp += '}' return cpp
def read_config(): _LOGGER.info("Reading configuration %s...", CORE.config_path) try: res = load_config() except EsphomeError as err: _LOGGER.error("Error while reading config: %s", err) return None if res.errors: if not CORE.verbose: res = strip_default_ids(res) safe_print(color('bold_red', "Failed config")) safe_print('') for path, domain in res.output_paths: if not res.is_in_error_path(path): continue safe_print(color('bold_red', f'{domain}:') + ' ' + (line_info(res.get_nested_item(path)) or '')) safe_print(indent(dump_dict(res, path)[0])) return None return OrderedDict(res)
def run(self, result: Config) -> None: if self.comp.config_schema is None: return with result.catch_error(self.path): if self.comp.is_platform: # Remove 'platform' key for validation input_conf = OrderedDict(self.conf) platform_val = input_conf.pop("platform") schema = cv.Schema(self.comp.config_schema) validated = 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(self.path, validated) else: schema = cv.Schema(self.comp.config_schema) validated = schema(self.conf) result.set_by_path(self.path, validated) result.add_validation_step(FinalValidateValidationStep(self.path, self.comp))
def show_new(value): from esphome import yaml_util for key in BINARY_SENSOR_REGISTRY: if key in value: break else: raise cv.Invalid( "This platform has been removed in 1.13, please see the docs for updated " "instructions.") val = value[key] args = [('platform', 'template')] if 'id' in value: args.append(('id', value['id'])) if 'name' in value: args.append(('name', value['name'])) args.append(('turn_on_action', { 'remote_transmitter.transmit_{}'.format(key): val })) text = yaml_util.dump([OrderedDict(args)]) raise cv.Invalid( u"This platform has been removed in 1.13, please change to:\n\n{}\n\n." u"".format(text))
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
def construct_yaml_map(self, node): """Traverses the given mapping node and returns a list of constructed key-value pairs.""" assert isinstance(node, yaml.MappingNode) # A list of key-value pairs we find in the current mapping pairs = [] # A list of key-value pairs we find while resolving merges ('<<' key), will be # added to pairs in a second pass merge_pairs = [] # A dict of seen keys so far, used to alert the user of duplicate keys and checking # which keys to merge. # Value of dict items is the start mark of the previous declaration. seen_keys = {} for key_node, value_node in node.value: # merge key is '<<' is_merge_key = key_node.tag == "tag:yaml.org,2002:merge" # key has no explicit tag set is_default_tag = key_node.tag == "tag:yaml.org,2002:value" if is_default_tag: # Default tag for mapping keys is string key_node.tag = "tag:yaml.org,2002:str" if not is_merge_key: # base case, this is a simple key-value pair key = self.construct_object(key_node) value = self.construct_object(value_node) # Check if key is hashable try: hash(key) except TypeError: # pylint: disable=raise-missing-from raise yaml.constructor.ConstructorError( f'Invalid key "{key}" (not hashable)', key_node.start_mark ) key = make_data_base(str(key)) key.from_node(key_node) # Check if it is a duplicate key if key in seen_keys: raise yaml.constructor.ConstructorError( f'Duplicate key "{key}"', key_node.start_mark, "NOTE: Previous declaration here:", seen_keys[key], ) seen_keys[key] = key_node.start_mark # Add to pairs pairs.append((key, value)) continue # This is a merge key, resolve value and add to merge_pairs value = self.construct_object(value_node) if isinstance(value, dict): # base case, copy directly to merge_pairs # direct merge, like "<<: {some_key: some_value}" merge_pairs.extend(value.items()) elif isinstance(value, list): # sequence merge, like "<<: [{some_key: some_value}, {other_key: some_value}]" for item in value: if not isinstance(item, dict): raise yaml.constructor.ConstructorError( "While constructing a mapping", node.start_mark, "Expected a mapping for merging, but found {}".format( type(item) ), value_node.start_mark, ) merge_pairs.extend(item.items()) else: raise yaml.constructor.ConstructorError( "While constructing a mapping", node.start_mark, "Expected a mapping or list of mappings for merging, " "but found {}".format(type(value)), value_node.start_mark, ) if merge_pairs: # We found some merge keys along the way, merge them into base pairs # https://yaml.org/type/merge.html # Construct a new merge set with values overridden by current mapping or earlier # sequence entries removed for key, value in merge_pairs: if key in seen_keys: # key already in the current map or from an earlier merge sequence entry, # do not override # # "... each of its key/value pairs is inserted into the current mapping, # unless the key already exists in it." # # "If the value associated with the merge key is a sequence, then this sequence # is expected to contain mapping nodes and each of these nodes is merged in # turn according to its order in the sequence. Keys in mapping nodes earlier # in the sequence override keys specified in later mapping nodes." continue pairs.append((key, value)) # Add key node to seen keys, for sequence merge values. seen_keys[key] = None return OrderedDict(pairs)
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