esphome/esphome/wizard.py

396 lines
11 KiB
Python

import os
import random
import string
import unicodedata
import voluptuous as vol
import esphome.config_validation as cv
from esphome.helpers import get_bool_env, write_file
from esphome.log import color, Fore
# pylint: disable=anomalous-backslash-in-string
from esphome.pins import ESP32_BOARD_PINS, ESP8266_BOARD_PINS
from esphome.storage_json import StorageJSON, ext_storage_path
from esphome.util import safe_print
from esphome.const import ALLOWED_NAME_CHARS, ENV_QUICKWIZARD
CORE_BIG = r""" _____ ____ _____ ______
/ ____/ __ \| __ \| ____|
| | | | | | |__) | |__
| | | | | | _ /| __|
| |___| |__| | | \ \| |____
\_____\____/|_| \_\______|
"""
ESP_BIG = r""" ______ _____ _____
| ____|/ ____| __ \\
| |__ | (___ | |__) |
| __| \___ \| ___/
| |____ ____) | |
|______|_____/|_|
"""
WIFI_BIG = r""" __ ___ ______ _
\ \ / (_) ____(_)
\ \ /\ / / _| |__ _
\ \/ \/ / | | __| | |
\ /\ / | | | | |
\/ \/ |_|_| |_|
"""
OTA_BIG = r""" ____ _______
/ __ \__ __|/\\
| | | | | | / \\
| | | | | | / /\ \\
| |__| | | |/ ____ \\
\____/ |_/_/ \_\\
"""
BASE_CONFIG = """esphome:
name: {name}
platform: {platform}
board: {board}
# Enable logging
logger:
# Enable Home Assistant API
api:
"""
def sanitize_double_quotes(value):
return value.replace("\\", "\\\\").replace('"', '\\"')
def wizard_file(**kwargs):
letters = string.ascii_letters + string.digits
ap_name_base = kwargs["name"].replace("_", " ").title()
ap_name = f"{ap_name_base} Fallback Hotspot"
if len(ap_name) > 32:
ap_name = ap_name_base
kwargs["fallback_name"] = ap_name
kwargs["fallback_psk"] = "".join(random.choice(letters) for _ in range(12))
config = BASE_CONFIG.format(**kwargs)
# Configure API
if "password" in kwargs:
config += ' password: "{0}"\n'.format(kwargs["password"])
# Configure OTA
config += "\nota:\n"
if "ota_password" in kwargs:
config += ' password: "{0}"'.format(kwargs["ota_password"])
elif "password" in kwargs:
config += ' password: "{0}"'.format(kwargs["password"])
# Configuring wifi
config += "\n\nwifi:\n"
if "ssid" in kwargs:
config += """ ssid: "{ssid}"
password: "{psk}"
""".format(
**kwargs
)
else:
config += """ # ssid: "My SSID"
# password: "mypassword"
networks:
"""
config += """
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "{fallback_name}"
password: "{fallback_psk}"
captive_portal:
""".format(
**kwargs
)
return config
def wizard_write(path, **kwargs):
name = kwargs["name"]
board = kwargs["board"]
for key in ("ssid", "psk", "password", "ota_password"):
if key in kwargs:
kwargs[key] = sanitize_double_quotes(kwargs[key])
if "platform" not in kwargs:
kwargs["platform"] = "ESP8266" if board in ESP8266_BOARD_PINS else "ESP32"
platform = kwargs["platform"]
write_file(path, wizard_file(**kwargs))
storage = StorageJSON.from_wizard(name, name + ".local", platform, board)
storage_path = ext_storage_path(os.path.dirname(path), os.path.basename(path))
storage.save(storage_path)
if get_bool_env(ENV_QUICKWIZARD):
def sleep(time):
pass
else:
from time import sleep
def safe_print_step(step, big):
safe_print()
safe_print()
safe_print(f"============= STEP {step} =============")
safe_print(big)
safe_print("===================================")
sleep(0.25)
def default_input(text, default):
safe_print()
safe_print(f"Press ENTER for default ({default})")
return input(text.format(default)) or default
# From https://stackoverflow.com/a/518232/8924614
def strip_accents(value):
return "".join(
c
for c in unicodedata.normalize("NFD", str(value))
if unicodedata.category(c) != "Mn"
)
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