def validator(value): if string_: value = string(value) value = value.replace(' ', space) if to_int: value = int_(value) if to_float: value = float_(value) if lower: value = Lower(value) if upper: value = Upper(value) if value not in values: import difflib options_ = [text_type(x) for x in values] option = text_type(value) matches = difflib.get_close_matches(option, options_) if matches: raise Invalid( u"Unknown value '{}', did you mean {}?" u"".format(value, u", ".join(u"'{}'".format(x) for x in matches))) raise Invalid(u"Unknown value '{}', valid options are {}.".format( value, options)) return value
def logger_log_action_to_code(config, action_id, template_arg, args): esp_log = LOG_LEVEL_TO_ESP_LOG[config[CONF_LEVEL]] args_ = [cg.RawExpression(text_type(x)) for x in config[CONF_ARGS]] text = text_type(cg.statement(esp_log(config[CONF_TAG], config[CONF_FORMAT], *args_))) lambda_ = yield cg.process_lambda(Lambda(text), args, return_type=cg.void) yield cg.new_Pvariable(action_id, template_arg, lambda_)
def logger_log_action_to_code(config, action_id, template_arg, args): esp_log = LOG_LEVEL_TO_ESP_LOG[config[CONF_LEVEL]] args_ = [RawExpression(text_type(x)) for x in config[CONF_ARGS]] text = text_type( statement(esp_log(config[CONF_TAG], config[CONF_FORMAT], *args_))) for lambda_ in process_lambda(Lambda(text), args, return_type=void): yield None rhs = LambdaAction.new(template_arg, lambda_) type = LambdaAction.template(template_arg) yield Pvariable(action_id, rhs, type=type)
def alphanumeric(value): if value is None: raise Invalid("string value is None") value = text_type(value) if not value.isalnum(): raise Invalid("string value is not alphanumeric") return value
def write_cpp(config): _LOGGER.info("Generating C++ source...") CORE.add_job(core_config.to_code, config[CONF_ESPHOME], domain='esphome') for domain in PRE_INITIALIZE: if domain == CONF_ESPHOME or domain not in config: continue CORE.add_job(get_component(domain).to_code, config[domain], domain=domain) for domain, component, conf in iter_components(config): if domain in PRE_INITIALIZE or not hasattr(component, 'to_code'): continue CORE.add_job(component.to_code, conf, domain=domain) CORE.flush_tasks() add(RawStatement('')) add(RawStatement('')) all_code = [] for exp in CORE.expressions: if not config[CONF_ESPHOME][CONF_USE_CUSTOM_CODE]: if isinstance(exp, Expression) and not exp.required: continue all_code.append(text_type(statement(exp))) writer.write_platformio_project() code_s = indent('\n'.join(line.rstrip() for line in all_code)) writer.write_cpp(code_s) return 0
def get_default(): # type: () -> EsphomeStorageJSON return EsphomeStorageJSON( storage_version=1, cookie_secret=text_type(binascii.hexlify(os.urandom(64))), last_update_check=None, remote_version=None, )
def read_config(args): while True: CORE.reset() data = json.loads(safe_input()) assert data['type'] == 'validate' CORE.vscode = True CORE.ace = args.ace f = data['file'] if CORE.ace: CORE.config_path = os.path.join(args.configuration[0], f) else: CORE.config_path = data['file'] vs = VSCodeResult() try: res = load_config() except Exception as err: # pylint: disable=broad-except vs.add_yaml_error(text_type(err)) else: for err in res.errors: try: range_ = _get_invalid_range(res, err) vs.add_validation_error(range_, _format_vol_invalid(err, res)) except Exception: # pylint: disable=broad-except continue print(vs.dump())
def preload_core_config(config): 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: raise EsphomeError(u"No esphome section in config") core_conf = config[CONF_ESPHOME] if CONF_PLATFORM not in core_conf: raise EsphomeError("esphome.platform not specified.") if CONF_BOARD not in core_conf: raise EsphomeError("esphome.board not specified.") if CONF_NAME not in core_conf: raise EsphomeError("esphome.name not specified.") try: CORE.esp_platform = validate_platform(core_conf[CONF_PLATFORM]) CORE.board = validate_board(core_conf[CONF_BOARD]) CORE.name = cv.valid_name(core_conf[CONF_NAME]) CORE.build_path = CORE.relative_path( cv.string(core_conf.get(CONF_BUILD_PATH, default_build_path()))) except vol.Invalid as e: raise EsphomeError(text_type(e))
def __str__(self): if self.i > 4294967295: return u'{}ULL'.format(self.i) if self.i > 2147483647: return u'{}UL'.format(self.i) if self.i < -2147483648: return u'{}LL'.format(self.i) return text_type(self.i)
def _secret_yaml(loader, node): """Load secrets and embed it into the configuration YAML.""" secret_path = os.path.join(os.path.dirname(loader.name), SECRET_YAML) secrets = _load_yaml_internal(secret_path) if node.value not in secrets: raise EsphomeError(u"Secret {} not defined".format(node.value)) val = secrets[node.value] _SECRET_VALUES[text_type(val)] = node.value return val
def cpp_global_section(self): from esphome.cpp_generator import statement global_code = [] for exp in self.global_statements: text = text_type(statement(exp)) text = text.rstrip() global_code.append(text) return u'\n'.join(global_code) + u'\n'
def construct_secret(self, node): secrets = _load_yaml_internal(self._rel_path(SECRET_YAML)) if node.value not in secrets: raise yaml.MarkedYAMLError( context=u"Secret '{}' not defined".format(node.value), context_mark=node.start_mark) val = secrets[node.value] _SECRET_VALUES[text_type(val)] = node.value return val
def humanize_error(config, validation_error): validation_error = text_type(validation_error) m = re.match(r'^(.*?)\s*(?:for dictionary value )?@ data\[.*$', validation_error) if m is not None: validation_error = m.group(1) validation_error = validation_error.strip() if not validation_error.endswith(u'.'): validation_error += u'.' return validation_error
def string_strict(value): """Like string, but only allows strings, and does not automatically convert other types to strings.""" check_not_templatable(value) if isinstance(value, text_type): return value if isinstance(value, string_types): return text_type(value) raise Invalid("Must be string, got {}. did you forget putting quotes " "around the value?".format(type(value)))
def validate_enum(enum_values, units=None, int=True): _units = [] if units is not None: _units = units if isinstance(units, list) else [units] _units = [text_type(x) for x in _units] enum_bound = cv.enum(enum_values, int=int) def validate_enum_bound(value): value = cv.string(value) for unit in _units: if value.endswith(unit): value = value[:-len(unit)] break return enum_bound(value) return validate_enum_bound
def _format_vol_invalid(ex, config): # type: (vol.Invalid, Config) -> unicode message = u'' paren = _get_parent_name(ex.path[:-1], config) if isinstance(ex, ExtraKeysInvalid): if ex.candidates: message += u'[{}] is an invalid option for [{}]. Did you mean {}?'.format( ex.path[-1], paren, u', '.join(u'[{}]'.format(x) for x in ex.candidates)) else: message += u'[{}] is an invalid option for [{}]. Please check the indentation.'.format( ex.path[-1], paren) elif u'extra keys not allowed' in text_type(ex): message += u'[{}] is an invalid option for [{}].'.format( ex.path[-1], paren) elif u'required key not provided' in text_type(ex): message += u"'{}' is a required option for [{}].".format( ex.path[-1], paren) else: message += humanize_error(config, ex) return message
def humanize_error(config, validation_error): offending_item_summary = _nested_getitem(config, validation_error.path) if isinstance(offending_item_summary, dict): offending_item_summary = None validation_error = text_type(validation_error) m = re.match(r'^(.*?)\s*(?:for dictionary value )?@ data\[.*$', validation_error) if m is not None: validation_error = m.group(1) validation_error = validation_error.strip() if not validation_error.endswith(u'.'): validation_error += u'.' if offending_item_summary is None or is_secret(offending_item_summary): return validation_error return u"{} Got '{}'".format(validation_error, offending_item_summary)
def string(value): """Validate that a configuration value is a string. If not, automatically converts to a string. Note that this can be lossy, for example the input value 60.00 (float) will be turned into "60.0" (string). For values where this could be a problem `string_string` has to be used. """ check_not_templatable(value) if isinstance(value, (dict, list)): raise Invalid("string value cannot be dictionary or list.") if isinstance(value, bool): raise Invalid("Auto-converted this value to boolean, please wrap the value in quotes.") if isinstance(value, text_type): return value if value is not None: return text_type(value) raise Invalid("string value is None")
def _send_message(self, msg): # type: (message.Message) -> None for message_type, klass in MESSAGE_TYPE_TO_PROTO.items(): if isinstance(msg, klass): break else: raise ValueError encoded = msg.SerializeToString() _LOGGER.debug("Sending %s:\n%s", type(msg), indent(text_type(msg))) if IS_PY2: req = chr(0x00) else: req = bytes([0]) req += _varuint_to_bytes(len(encoded)) req += _varuint_to_bytes(message_type) req += encoded self._write(req)
def represent_float(self, value): if is_secret(value): return self.represent_secret(value) if math.isnan(value): value = u'.nan' elif math.isinf(value): value = u'.inf' if value > 0 else u'-.inf' else: value = text_type(repr(value)).lower() # Note that in some cases `repr(data)` represents a float number # without the decimal parts. For instance: # >>> repr(1e17) # '1e17' # Unfortunately, this is not a valid float representation according # to the definition of the `!!float` tag. We fix this by adding # '.0' before the 'e' symbol. if u'.' not in value and u'e' in value: value = value.replace(u'e', u'.0e', 1) return self.represent_scalar(tag=u'tag:yaml.org,2002:float', value=value)
def register_component(var, config): """Register the given obj as a component. This is a coroutine, you must await it with a 'yield' expression! :param var: The variable representing the component. :param config: The configuration for the component. """ id_ = text_type(var.base) if id_ not in CORE.component_ids: raise ValueError( u"Component ID {} was not declared to inherit from Component, " u"or was registered twice. Please create a bug report with your " u"configuration.".format(id_)) CORE.component_ids.remove(id_) if CONF_SETUP_PRIORITY in config: add(var.set_setup_priority(config[CONF_SETUP_PRIORITY])) if CONF_UPDATE_INTERVAL in config: add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) add(App.register_component(var)) yield var
def __str__(self): text = u", ".join(text_type(x) for x in self.args) return indent_all_but_first_and_last(text)
def strip_accents(value): return u''.join(c for c in unicodedata.normalize('NFD', text_type(value)) if unicodedata.category(c) != 'Mn')
def dump_dict(config, path, at_root=True): # type: (Config, ConfigPath, bool) -> Tuple[unicode, bool] conf = config.get_nested_item(path) ret = u'' multiline = False if at_root: error = config.get_error_for_path(path) if error is not None: ret += u'\n' + color('bold_red', _format_vol_invalid( error, config)) + u'\n' if isinstance(conf, (list, tuple)): multiline = True if not conf: ret += u'[]' multiline = False for i in range(len(conf)): path_ = path + [i] error = config.get_error_for_path(path_) if error is not None: ret += u'\n' + color( 'bold_red', _format_vol_invalid(error, config)) + u'\n' sep = u'- ' if config.is_in_error_path(path_): sep = color('red', sep) msg, _ = dump_dict(config, path_, at_root=False) msg = indent(msg) inf = line_info(config.get_nested_item(path_), highlight=config.is_in_error_path(path_)) if inf is not None: msg = inf + u'\n' + msg elif msg: msg = msg[2:] ret += sep + msg + u'\n' elif isinstance(conf, dict): multiline = True if not conf: ret += u'{}' multiline = False for k in conf.keys(): path_ = path + [k] error = config.get_error_for_path(path_) if error is not None: ret += u'\n' + color( 'bold_red', _format_vol_invalid(error, config)) + u'\n' st = u'{}: '.format(k) if config.is_in_error_path(path_): st = color('red', st) msg, m = dump_dict(config, path_, at_root=False) inf = line_info(config.get_nested_item(path_), highlight=config.is_in_error_path(path_)) if m: msg = u'\n' + indent(msg) if inf is not None: if m: msg = u' ' + inf + msg else: msg = msg + u' ' + inf ret += st + msg + u'\n' elif isinstance(conf, str): if is_secret(conf): conf = u'!secret {}'.format(is_secret(conf)) if not conf: conf += u"''" if len(conf) > 80: conf = u'|-\n' + indent(conf) error = config.get_error_for_path(path) col = 'bold_red' if error else 'white' ret += color(col, text_type(conf)) elif isinstance(conf, core.Lambda): if is_secret(conf): conf = u'!secret {}'.format(is_secret(conf)) conf = u'!lambda |-\n' + indent(text_type(conf.value)) error = config.get_error_for_path(path) col = 'bold_red' if error else 'white' ret += color(col, conf) elif conf is None: pass else: error = config.get_error_for_path(path) col = 'bold_red' if error else 'white' ret += color(col, text_type(conf)) multiline = u'\n' in ret return ret, multiline
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 do_id_pass(result): # type: (Config) -> None from esphome.cpp_generator import MockObjClass from esphome.cpp_types import Component declare_ids = [] # type: List[Tuple[core.ID, ConfigPath]] searching_ids = [] # type: List[Tuple[core.ID, ConfigPath]] for id, path in iter_ids(result): if id.is_declaration: if id.id is not None: # Look for duplicate definitions match = next((v for v in declare_ids if v[0].id == id.id), None) if match is not None: opath = u'->'.join(text_type(v) for v in match[1]) result.add_str_error( u"ID {} redefined! Check {}".format(id.id, opath), path) continue declare_ids.append((id, path)) else: searching_ids.append((id, path)) # Resolve default ids after manual IDs for id, _ in declare_ids: id.resolve([v[0].id for v in declare_ids]) if isinstance(id.type, MockObjClass) and id.type.inherits_from(Component): CORE.component_ids.add(id.id) # Check searched IDs for id, path in searching_ids: if id.id is not None: # manually declared match = next((v[0] for v in declare_ids if v[0].id == id.id), None) if match is None: # No declared ID with this name import difflib error = ( "Couldn't find ID '{}'. Please check you have defined " "an ID with that name in your configuration.".format( id.id)) # Find candidates matches = difflib.get_close_matches( id.id, [v[0].id for v in declare_ids]) if matches: matches_s = ', '.join('"{}"'.format(x) for x in matches) error += " These IDs look similar: {}.".format(matches_s) result.add_str_error(error, path) continue if not isinstance(match.type, MockObjClass) or not isinstance( id.type, MockObjClass): continue if not match.type.inherits_from(id.type): result.add_str_error( "ID '{}' of type {} doesn't inherit from {}. Please " "double check your ID is pointing to the correct value" "".format(id.id, match.type, id.type), path) if id.id is None and id.type is not None: for v in declare_ids: if v[0] is None or not isinstance(v[0].type, MockObjClass): continue inherits = v[0].type.inherits_from(id.type) if inherits: id.id = v[0].id break else: result.add_str_error( "Couldn't resolve ID for type '{}'".format(id.type), path)
def represent_secret(value): return yaml.ScalarNode(tag=u'!secret', value=_SECRET_VALUES[text_type(value)])
def _lambda(loader, node): return Lambda(text_type(node.value))
def is_secret(value): try: return _SECRET_VALUES[text_type(value)] except (KeyError, ValueError): return None
def add_error(self, message, path): # type: (basestring, ConfigPath) -> None if not isinstance(message, text_type): message = text_type(message) self.errors.append((message, path))