def read_config(command_line_substitutions): _LOGGER.info("Reading configuration %s...", CORE.config_path) try: res = load_config(command_line_substitutions) 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(Fore.BOLD_RED, "Failed config")) safe_print("") for path, domain in res.output_paths: if not res.is_in_error_path(path): continue errstr = color(Fore.BOLD_RED, f"{domain}:") errline = line_info(res, path) if errline: errstr += " " + errline safe_print(errstr) safe_print(indent(dump_dict(res, path)[0])) return None return OrderedDict(res)
def choose_prompt(options): if not options: raise EsphomeError( "Found no valid options for upload/logging, please make sure relevant " "sections (ota, api, mqtt, ...) are in your configuration and/or the " "device is plugged in.") if len(options) == 1: return options[0][1] safe_print("Found multiple options, please choose one:") for i, (desc, _) in enumerate(options): safe_print(f" [{i+1}] {desc}") while True: opt = input("(number): ") if opt in options: opt = options.index(opt) break try: opt = int(opt) if opt < 1 or opt > len(options): raise ValueError break except ValueError: safe_print(color(Fore.RED, f"Invalid option: '{opt}'")) return options[opt - 1][1]
def line_info(config, path, highlight=True): """Display line config source.""" if not highlight: return None obj = config.get_deepest_document_range_for_path(path) if obj: mark = obj.start_mark source = "[source {}:{}]".format(mark.document, mark.line + 1) return color(Fore.CYAN, source) return "None"
def on_log(msg): time_ = datetime.now().time().strftime("[%H:%M:%S]") text = msg.message if msg.send_failed: text = color( Fore.WHITE, "(Message skipped because it was too big to fit in " "TCP buffer - This is only cosmetic)", ) safe_print(time_ + text)
def get_fingerprint(config): addr = str(config[CONF_MQTT][CONF_BROKER]), int(config[CONF_MQTT][CONF_PORT]) _LOGGER.info("Getting fingerprint from %s:%s", addr[0], addr[1]) try: cert_pem = ssl.get_server_certificate(addr) except OSError as err: _LOGGER.error("Unable to connect to server: %s", err) return 1 cert_der = ssl.PEM_cert_to_DER_cert(cert_pem) sha1 = hashlib.sha1(cert_der).hexdigest() safe_print("SHA1 Fingerprint: " + color(Fore.CYAN, sha1)) safe_print( "Copy the string above into mqtt.ssl_fingerprints section of {}" "".format(CORE.config_path) ) return 0
def command_update_all(args): import click success = {} files = list_yaml_files(args.configuration[0]) twidth = 60 def print_bar(middle_text): middle_text = f" {middle_text} " width = len(click.unstyle(middle_text)) half_line = "=" * ((twidth - width) // 2) click.echo(f"{half_line}{middle_text}{half_line}") for f in files: print("Updating {}".format(color(Fore.CYAN, f))) print("-" * twidth) print() rc = run_external_process("esphome", "--dashboard", "run", "--no-logs", "--device", "OTA", f) if rc == 0: print_bar("[{}] {}".format(color(Fore.BOLD_GREEN, "SUCCESS"), f)) success[f] = True else: print_bar("[{}] {}".format(color(Fore.BOLD_RED, "ERROR"), f)) success[f] = False print() print() print() print_bar("[{}]".format(color(Fore.BOLD_WHITE, "SUMMARY"))) failed = 0 for f in files: if success[f]: print(" - {}: {}".format(f, color(Fore.GREEN, "SUCCESS"))) else: print(" - {}: {}".format(f, color(Fore.BOLD_RED, "FAILED"))) failed += 1 return failed
def dump_dict(config, path, at_root=True): # type: (Config, ConfigPath, bool) -> Tuple[str, bool] conf = config.get_nested_item(path) ret = "" multiline = False if at_root: error = config.get_error_for_path(path) if error is not None: ret += ("\n" + color(Fore.BOLD_RED, _format_vol_invalid(error, config)) + "\n") if isinstance(conf, (list, tuple)): multiline = True if not conf: ret += "[]" multiline = False for i in range(len(conf)): path_ = path + [i] error = config.get_error_for_path(path_) if error is not None: ret += ("\n" + color( Fore.BOLD_RED, _format_vol_invalid(error, config)) + "\n") sep = "- " if config.is_in_error_path(path_): sep = color(Fore.RED, sep) msg, _ = dump_dict(config, path_, at_root=False) msg = indent(msg) inf = line_info(config, path_, highlight=config.is_in_error_path(path_)) if inf is not None: msg = inf + "\n" + msg elif msg: msg = msg[2:] ret += sep + msg + "\n" elif isinstance(conf, dict): multiline = True if not conf: ret += "{}" multiline = False for k in conf.keys(): path_ = path + [k] error = config.get_error_for_path(path_) if error is not None: ret += ("\n" + color( Fore.BOLD_RED, _format_vol_invalid(error, config)) + "\n") st = f"{k}: " if config.is_in_error_path(path_): st = color(Fore.RED, st) msg, m = dump_dict(config, path_, at_root=False) inf = line_info(config, path_, highlight=config.is_in_error_path(path_)) if m: msg = "\n" + indent(msg) if inf is not None: if m: msg = " " + inf + msg else: msg = msg + " " + inf ret += st + msg + "\n" elif isinstance(conf, str): if is_secret(conf): conf = "!secret {}".format(is_secret(conf)) if not conf: conf += "''" if len(conf) > 80: conf = "|-\n" + indent(conf) error = config.get_error_for_path(path) col = Fore.BOLD_RED if error else Fore.KEEP ret += color(col, str(conf)) elif isinstance(conf, core.Lambda): if is_secret(conf): conf = "!secret {}".format(is_secret(conf)) conf = "!lambda |-\n" + indent(str(conf.value)) error = config.get_error_for_path(path) col = Fore.BOLD_RED if error else Fore.KEEP ret += color(col, conf) elif conf is None: pass else: error = config.get_error_for_path(path) col = Fore.BOLD_RED if error else Fore.KEEP ret += color(col, str(conf)) multiline = "\n" in ret return ret, multiline
def wizard(path): if not path.endswith(".yaml") and not path.endswith(".yml"): safe_print( "Please make your configuration file {} have the extension .yaml or .yml" "".format(color(Fore.CYAN, path))) return 1 if os.path.exists(path): safe_print( "Uh oh, it seems like {} already exists, please delete that file first " "or chose another configuration file.".format( color(Fore.CYAN, path))) return 2 safe_print("Hi there!") sleep(1.5) safe_print("I'm the wizard of ESPHome :)") sleep(1.25) safe_print("And I'm here to help you get started with ESPHome.") sleep(2.0) safe_print( "In 4 steps I'm going to guide you through creating a basic " "configuration file for your custom ESP8266/ESP32 firmware. Yay!") sleep(3.0) safe_print() safe_print_step(1, CORE_BIG) safe_print("First up, please choose a " + color(Fore.GREEN, "name") + " for your node.") safe_print( "It should be a unique name that can be used to identify the device later." ) sleep(1) safe_print( "For example, I like calling the node in my living room {}.".format( color(Fore.BOLD_WHITE, "livingroom"))) safe_print() sleep(1) name = input(color(Fore.BOLD_WHITE, "(name): ")) while True: try: name = cv.valid_name(name) break except vol.Invalid: safe_print( color( Fore.RED, f'Oh noes, "{name}" isn\'t a valid name. Names can only ' f"include numbers, lower-case letters and hyphens. ", )) name = strip_accents(name).lower().replace(" ", "-") name = strip_accents(name).lower().replace("_", "-") name = "".join(c for c in name if c in ALLOWED_NAME_CHARS) safe_print('Shall I use "{}" as the name instead?'.format( color(Fore.CYAN, name))) sleep(0.5) name = default_input("(name [{}]): ", name) safe_print('Great! Your node is now called "{}".'.format( color(Fore.CYAN, name))) sleep(1) safe_print_step(2, ESP_BIG) safe_print( "Now I'd like to know what microcontroller you're using so that I can compile " "firmwares for it.") safe_print("Are you using an " + color(Fore.GREEN, "ESP32") + " or " + color(Fore.GREEN, "ESP8266") + " platform? (Choose ESP8266 for Sonoff devices)") while True: sleep(0.5) safe_print() safe_print("Please enter either ESP32 or ESP8266.") platform = input(color(Fore.BOLD_WHITE, "(ESP32/ESP8266): ")) try: platform = vol.All(vol.Upper, vol.Any("ESP32", "ESP8266"))(platform) break except vol.Invalid: safe_print( "Unfortunately, I can't find an espressif microcontroller called " '"{}". Please try again.'.format(platform)) safe_print("Thanks! You've chosen {} as your platform.".format( color(Fore.CYAN, platform))) safe_print() sleep(1) if platform == "ESP32": board_link = ( "http://docs.platformio.org/en/latest/platforms/espressif32.html#boards" ) else: board_link = ( "http://docs.platformio.org/en/latest/platforms/espressif8266.html#boards" ) safe_print("Next, I need to know what " + color(Fore.GREEN, "board") + " you're using.") sleep(0.5) safe_print("Please go to {} and choose a board.".format( color(Fore.GREEN, board_link))) if platform == "ESP32": safe_print("(Type " + color(Fore.GREEN, "esp01_1m") + " for Sonoff devices)") safe_print() # Don't sleep because user needs to copy link if platform == "ESP32": safe_print('For example "{}".'.format( color(Fore.BOLD_WHITE, "nodemcu-32s"))) boards = list(ESP32_BOARD_PINS.keys()) else: safe_print('For example "{}".'.format( color(Fore.BOLD_WHITE, "nodemcuv2"))) boards = list(ESP8266_BOARD_PINS.keys()) safe_print("Options: {}".format(", ".join(sorted(boards)))) while True: board = input(color(Fore.BOLD_WHITE, "(board): ")) try: board = vol.All(vol.Lower, vol.Any(*boards))(board) break except vol.Invalid: safe_print( color(Fore.RED, f'Sorry, I don\'t think the board "{board}" exists.')) safe_print() sleep(0.25) safe_print() safe_print("Way to go! You've chosen {} as your board.".format( color(Fore.CYAN, board))) safe_print() sleep(1) safe_print_step(3, WIFI_BIG) safe_print("In this step, I'm going to create the configuration for " "WiFi.") safe_print() sleep(1) safe_print("First, what's the " + color(Fore.GREEN, "SSID") + f" (the name) of the WiFi network {name} should connect to?") sleep(1.5) safe_print('For example "{}".'.format( color(Fore.BOLD_WHITE, "Abraham Linksys"))) while True: ssid = input(color(Fore.BOLD_WHITE, "(ssid): ")) try: ssid = cv.ssid(ssid) break except vol.Invalid: safe_print( color( Fore.RED, 'Unfortunately, "{}" doesn\'t seem to be a valid SSID. ' "Please try again.".format(ssid), )) safe_print() sleep(1) safe_print('Thank you very much! You\'ve just chosen "{}" as your SSID.' "".format(color(Fore.CYAN, ssid))) safe_print() sleep(0.75) safe_print( "Now please state the " + color(Fore.GREEN, "password") + " of the WiFi network so that I can connect to it (Leave empty for no password)" ) safe_print() safe_print('For example "{}"'.format(color(Fore.BOLD_WHITE, "PASSWORD42"))) sleep(0.5) psk = input(color(Fore.BOLD_WHITE, "(PSK): ")) safe_print( "Perfect! WiFi is now set up (you can create static IPs and so on later)." ) sleep(1.5) safe_print_step(4, OTA_BIG) safe_print( "Almost there! ESPHome can automatically upload custom firmwares over WiFi " "(over the air) and integrates into Home Assistant with a native API.") safe_print( "This can be insecure if you do not trust the WiFi network. Do you want to set " "a " + color(Fore.GREEN, "password") + " for connecting to this ESP?") safe_print() sleep(0.25) safe_print("Press ENTER for no password") password = input(color(Fore.BOLD_WHITE, "(password): ")) wizard_write( path=path, name=name, platform=platform, board=board, ssid=ssid, psk=psk, password=password, ) safe_print() safe_print( color(Fore.CYAN, "DONE! I've now written a new configuration file to ") + color(Fore.BOLD_CYAN, path)) safe_print() safe_print("Next steps:") safe_print( ' > Check your Home Assistant "integrations" screen. If all goes well, you ' "should see your ESP being discovered automatically.") safe_print(" > Then follow the rest of the getting started guide:") safe_print( " > https://esphome.io/guides/getting_started_command_line.html") return 0
def command_rename(args, config): for c in args.name: if c not in ALLOWED_NAME_CHARS: print( color( Fore.BOLD_RED, f"'{c}' is an invalid character for names. Valid characters are: " f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)", )) return 1 # Load existing yaml file with open(CORE.config_path, mode="r+", encoding="utf-8") as raw_file: raw_contents = raw_file.read() yaml = yaml_util.load_yaml(CORE.config_path) if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]: print( color(Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed.")) return 1 old_name = yaml[CONF_ESPHOME][CONF_NAME] match = re.match(r"^\$\{?([a-zA-Z0-9_]+)\}?$", old_name) if match is None: new_raw = re.sub( rf"name:\s+[\"']?{old_name}[\"']?", f'name: "{args.name}"', raw_contents, ) else: old_name = yaml[CONF_SUBSTITUTIONS][match.group(1)] if (len( re.findall( rf"^\s+{match.group(1)}:\s+[\"']?{old_name}[\"']?", raw_contents, flags=re.MULTILINE, )) > 1): print( color(Fore.BOLD_RED, "Too many matches in YAML to safely rename")) return 1 new_raw = re.sub( rf"^(\s+{match.group(1)}):\s+[\"']?{old_name}[\"']?", f'\\1: "{args.name}"', raw_contents, flags=re.MULTILINE, ) new_path = os.path.join(CORE.config_dir, args.name + ".yaml") print( f"Updating {color(Fore.CYAN, CORE.config_path)} to {color(Fore.CYAN, new_path)}" ) print() with open(new_path, mode="w", encoding="utf-8") as new_file: new_file.write(new_raw) rc = run_external_process("esphome", "config", new_path) if rc != 0: print(color(Fore.BOLD_RED, "Rename failed. Reverting changes.")) os.remove(new_path) return 1 cli_args = [ "run", new_path, "--no-logs", "--device", CORE.address, ] if args.dashboard: cli_args.insert(0, "--dashboard") try: rc = run_external_process("esphome", *cli_args) except KeyboardInterrupt: rc = 1 if rc != 0: os.remove(new_path) return 1 os.remove(CORE.config_path) print(color(Fore.BOLD_GREEN, "SUCCESS")) print() return 0