From 33625e2dd3b7fc85dd6fe2b45ba4d898a560bfec Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Tue, 8 Jun 2021 01:14:12 +0200 Subject: [PATCH] CLI user experience improvements (#1805) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .github/workflows/release-dev.yml | 2 +- .github/workflows/release.yml | 2 +- .vscode/tasks.json | 2 +- docker/Dockerfile | 2 +- docker/rootfs/etc/services.d/esphome/run | 2 +- esphome/__main__.py | 236 ++++++++++++++++------- esphome/dashboard/dashboard.py | 24 +-- script/test | 8 +- 9 files changed, 187 insertions(+), 93 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2230b3da7..68e308d2bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,7 +126,7 @@ jobs: run: | echo "::add-matcher::.github/workflows/matchers/gcc.json" echo "::add-matcher::.github/workflows/matchers/python.json" - - run: esphome tests/${{ matrix.test }}.yaml compile + - run: esphome compile tests/${{ matrix.test }}.yaml pytest: runs-on: ubuntu-latest diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index 1a88a362f8..51606ecad0 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -123,7 +123,7 @@ jobs: run: | echo "::add-matcher::.github/workflows/matchers/gcc.json" echo "::add-matcher::.github/workflows/matchers/python.json" - - run: esphome tests/${{ matrix.test }}.yaml compile + - run: esphome compile tests/${{ matrix.test }}.yaml pytest: runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fff89040ba..2891959d35 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -121,7 +121,7 @@ jobs: run: | echo "::add-matcher::.github/workflows/matchers/gcc.json" echo "::add-matcher::.github/workflows/matchers/python.json" - - run: esphome tests/${{ matrix.test }}.yaml compile + - run: esphome compile tests/${{ matrix.test }}.yaml pytest: runs-on: ubuntu-latest diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e11600b093..cc83d8bcdf 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,7 +4,7 @@ { "label": "run", "type": "shell", - "command": "python3 -m esphome config dashboard", + "command": "python3 -m esphome dashboard config", "problemMatcher": [] } ] diff --git a/docker/Dockerfile b/docker/Dockerfile index 927c62cf42..ef547fc0f1 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -27,4 +27,4 @@ WORKDIR /config # in every docker command twice ENTRYPOINT ["esphome"] # When no arguments given, start the dashboard in the workdir -CMD ["/config", "dashboard"] +CMD ["dashboard", "/config"] diff --git a/docker/rootfs/etc/services.d/esphome/run b/docker/rootfs/etc/services.d/esphome/run index 6257bec6a3..f806c50929 100755 --- a/docker/rootfs/etc/services.d/esphome/run +++ b/docker/rootfs/etc/services.d/esphome/run @@ -23,4 +23,4 @@ if bashio::config.has_value 'relative_url'; then fi bashio::log.info "Starting ESPHome dashboard..." -exec esphome /config/esphome dashboard --socket /var/run/esphome.sock --hassio +exec esphome dashboard /config/esphome --socket /var/run/esphome.sock --hassio diff --git a/esphome/__main__.py b/esphome/__main__.py index b78962c2c0..9af08a8f21 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -268,7 +268,7 @@ def clean_mqtt(config, args): def command_wizard(args): from esphome import wizard - return wizard.wizard(args.configuration[0]) + return wizard.wizard(args.configuration) def command_config(args, config): @@ -284,7 +284,7 @@ def command_vscode(args): logging.disable(logging.INFO) logging.disable(logging.WARNING) - CORE.config_path = args.configuration[0] + CORE.config_path = args.configuration vscode.read_config(args) @@ -304,7 +304,7 @@ def command_compile(args, config): def command_upload(args, config): port = choose_upload_log_host( - default=args.upload_port, + default=args.device, check_default=None, show_ota=True, show_mqtt=False, @@ -319,7 +319,7 @@ def command_upload(args, config): def command_logs(args, config): port = choose_upload_log_host( - default=args.serial_port, + default=args.device, check_default=None, show_ota=False, show_mqtt=True, @@ -337,7 +337,7 @@ def command_run(args, config): return exit_code _LOGGER.info("Successfully compiled program.") port = choose_upload_log_host( - default=args.upload_port, + default=args.device, check_default=None, show_ota=True, show_mqtt=False, @@ -350,7 +350,7 @@ def command_run(args, config): if args.no_logs: return 0 port = choose_upload_log_host( - default=args.upload_port, + default=args.device, check_default=port, show_ota=False, show_mqtt=True, @@ -394,7 +394,7 @@ def command_update_all(args): import click success = {} - files = list_yaml_files(args.configuration[0]) + files = list_yaml_files(args.configuration) twidth = 60 def print_bar(middle_text): @@ -408,7 +408,7 @@ def command_update_all(args): print("-" * twidth) print() rc = run_external_process( - "esphome", "--dashboard", f, "run", "--no-logs", "--upload-port", "OTA" + "esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA" ) if rc == 0: print_bar("[{}] {}".format(color(Fore.BOLD_GREEN, "SUCCESS"), f)) @@ -453,15 +453,17 @@ POST_CONFIG_ACTIONS = { def parse_args(argv): - parser = argparse.ArgumentParser(description=f"ESPHome v{const.__version__}") - parser.add_argument( - "-v", "--verbose", help="Enable verbose esphome logs.", action="store_true" + options_parser = argparse.ArgumentParser(add_help=False) + options_parser.add_argument( + "-v", "--verbose", help="Enable verbose ESPHome logs.", action="store_true" ) - parser.add_argument( - "-q", "--quiet", help="Disable all esphome logs.", action="store_true" + options_parser.add_argument( + "-q", "--quiet", help="Disable all ESPHome logs.", action="store_true" ) - parser.add_argument("--dashboard", help=argparse.SUPPRESS, action="store_true") - parser.add_argument( + options_parser.add_argument( + "--dashboard", help=argparse.SUPPRESS, action="store_true" + ) + options_parser.add_argument( "-s", "--substitution", nargs=2, @@ -469,17 +471,87 @@ def parse_args(argv): help="Add a substitution", metavar=("key", "value"), ) - parser.add_argument( - "configuration", help="Your YAML configuration file.", nargs="*" + + # Keep backward compatibility with the old command line format of + # esphome . + # + # Unfortunately this can't be done by adding another configuration argument to the + # main config parser, as argparse is greedy when parsing arguments, so in regular + # usage it'll eat the command as the configuration argument and error out out + # because it can't parse the configuration as a command. + # + # Instead, construct an ad-hoc parser for the old format that doesn't actually + # process the arguments, but parses them enough to let us figure out if the old + # format is used. In that case, swap the command and configuration in the arguments + # and continue on with the normal parser (after raising a deprecation warning). + # + # Disable argparse's built-in help option and add it manually to prevent this + # parser from printing the help messagefor the old format when invoked with -h. + compat_parser = argparse.ArgumentParser(parents=[options_parser], add_help=False) + compat_parser.add_argument("-h", "--help") + compat_parser.add_argument("configuration", nargs="*") + compat_parser.add_argument( + "command", + choices=[ + "config", + "compile", + "upload", + "logs", + "run", + "clean-mqtt", + "wizard", + "mqtt-fingerprint", + "version", + "clean", + "dashboard", + ], ) - subparsers = parser.add_subparsers(help="Commands", dest="command") + # on Python 3.9+ we can simply set exit_on_error=False in the constructor + def _raise(x): + raise argparse.ArgumentError(None, x) + + compat_parser.error = _raise + + try: + result, unparsed = compat_parser.parse_known_args(argv[1:]) + last_option = len(argv) - len(unparsed) - 1 - len(result.configuration) + argv = argv[0:last_option] + [result.command] + result.configuration + unparsed + deprecated_argv_suggestion = argv + except argparse.ArgumentError: + # This is not an old-style command line, so we don't have to do anything. + deprecated_argv_suggestion = None + + # And continue on with regular parsing + parser = argparse.ArgumentParser( + description=f"ESPHome v{const.__version__}", parents=[options_parser] + ) + parser.set_defaults(deprecated_argv_suggestion=deprecated_argv_suggestion) + + mqtt_options = argparse.ArgumentParser(add_help=False) + mqtt_options.add_argument("--topic", help="Manually set the MQTT topic.") + mqtt_options.add_argument("--username", help="Manually set the MQTT username.") + mqtt_options.add_argument("--password", help="Manually set the MQTT password.") + mqtt_options.add_argument("--client-id", help="Manually set the MQTT client id.") + + subparsers = parser.add_subparsers( + help="Command to run:", dest="command", metavar="command" + ) subparsers.required = True - subparsers.add_parser("config", help="Validate the configuration and spit it out.") + + parser_config = subparsers.add_parser( + "config", help="Validate the configuration and spit it out." + ) + parser_config.add_argument( + "configuration", help="Your YAML configuration file(s).", nargs="+" + ) parser_compile = subparsers.add_parser( "compile", help="Read the configuration and compile a program." ) + parser_compile.add_argument( + "configuration", help="Your YAML configuration file(s).", nargs="+" + ) parser_compile.add_argument( "--only-generate", help="Only generate source code, do not compile.", @@ -487,106 +559,124 @@ def parse_args(argv): ) parser_upload = subparsers.add_parser( - "upload", help="Validate the configuration " "and upload the latest binary." + "upload", help="Validate the configuration and upload the latest binary." ) parser_upload.add_argument( - "--upload-port", - help="Manually specify the upload port to use. " - "For example /dev/cu.SLAB_USBtoUART.", + "configuration", help="Your YAML configuration file(s).", nargs="+" + ) + parser_upload.add_argument( + "--device", + help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.", ) parser_logs = subparsers.add_parser( - "logs", help="Validate the configuration " "and show all MQTT logs." + "logs", + help="Validate the configuration and show all logs.", + parents=[mqtt_options], ) - parser_logs.add_argument("--topic", help="Manually set the topic to subscribe to.") - parser_logs.add_argument("--username", help="Manually set the username.") - parser_logs.add_argument("--password", help="Manually set the password.") - parser_logs.add_argument("--client-id", help="Manually set the client id.") parser_logs.add_argument( - "--serial-port", - help="Manually specify a serial port to use" - "For example /dev/cu.SLAB_USBtoUART.", + "configuration", help="Your YAML configuration file.", nargs=1 + ) + parser_logs.add_argument( + "--device", + help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.", ) parser_run = subparsers.add_parser( "run", - help="Validate the configuration, create a binary, " - "upload it, and start MQTT logs.", + help="Validate the configuration, create a binary, upload it, and start logs.", + parents=[mqtt_options], ) parser_run.add_argument( - "--upload-port", - help="Manually specify the upload port/ip to use. " - "For example /dev/cu.SLAB_USBtoUART.", + "configuration", help="Your YAML configuration file(s).", nargs="+" ) parser_run.add_argument( - "--no-logs", help="Disable starting MQTT logs.", action="store_true" + "--device", + help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.", ) parser_run.add_argument( - "--topic", help="Manually set the topic to subscribe to for logs." + "--no-logs", help="Disable starting logs.", action="store_true" ) - parser_run.add_argument( - "--username", help="Manually set the MQTT username for logs." - ) - parser_run.add_argument( - "--password", help="Manually set the MQTT password for logs." - ) - parser_run.add_argument("--client-id", help="Manually set the client id for logs.") parser_clean = subparsers.add_parser( - "clean-mqtt", help="Helper to clear an MQTT topic from " "retain messages." + "clean-mqtt", + help="Helper to clear retained messages from an MQTT topic.", + parents=[mqtt_options], + ) + parser_clean.add_argument( + "configuration", help="Your YAML configuration file(s).", nargs="+" ) - parser_clean.add_argument("--topic", help="Manually set the topic to subscribe to.") - parser_clean.add_argument("--username", help="Manually set the username.") - parser_clean.add_argument("--password", help="Manually set the password.") - parser_clean.add_argument("--client-id", help="Manually set the client id.") - subparsers.add_parser( + parser_wizard = subparsers.add_parser( "wizard", - help="A helpful setup wizard that will guide " - "you through setting up esphome.", + help="A helpful setup wizard that will guide you through setting up ESPHome.", + ) + parser_wizard.add_argument( + "configuration", + help="Your YAML configuration file.", ) - subparsers.add_parser( + parser_fingerprint = subparsers.add_parser( "mqtt-fingerprint", help="Get the SSL fingerprint from a MQTT broker." ) + parser_fingerprint.add_argument( + "configuration", help="Your YAML configuration file(s).", nargs="+" + ) - subparsers.add_parser("version", help="Print the esphome version and exit.") + subparsers.add_parser("version", help="Print the ESPHome version and exit.") - subparsers.add_parser("clean", help="Delete all temporary build files.") + parser_clean = subparsers.add_parser( + "clean", help="Delete all temporary build files." + ) + parser_clean.add_argument( + "configuration", help="Your YAML configuration file(s).", nargs="+" + ) - dashboard = subparsers.add_parser( + parser_dashboard = subparsers.add_parser( "dashboard", help="Create a simple web server for a dashboard." ) - dashboard.add_argument( + parser_dashboard.add_argument( + "configuration", + help="Your YAML configuration file directory.", + ) + parser_dashboard.add_argument( "--port", help="The HTTP port to open connections on. Defaults to 6052.", type=int, default=6052, ) - dashboard.add_argument( + parser_dashboard.add_argument( "--username", - help="The optional username to require " "for authentication.", + help="The optional username to require for authentication.", type=str, default="", ) - dashboard.add_argument( + parser_dashboard.add_argument( "--password", - help="The optional password to require " "for authentication.", + help="The optional password to require for authentication.", type=str, default="", ) - dashboard.add_argument( + parser_dashboard.add_argument( "--open-ui", help="Open the dashboard UI in a browser.", action="store_true" ) - dashboard.add_argument("--hassio", help=argparse.SUPPRESS, action="store_true") - dashboard.add_argument( + parser_dashboard.add_argument( + "--hassio", help=argparse.SUPPRESS, action="store_true" + ) + parser_dashboard.add_argument( "--socket", help="Make the dashboard serve under a unix socket", type=str ) - vscode = subparsers.add_parser("vscode", help=argparse.SUPPRESS) - vscode.add_argument("--ace", action="store_true") + parser_vscode = subparsers.add_parser("vscode") + parser_vscode.add_argument( + "configuration", help="Your YAML configuration file.", nargs=1 + ) + parser_vscode.add_argument("--ace", action="store_true") - subparsers.add_parser("update-all", help=argparse.SUPPRESS) + parser_update = subparsers.add_parser("update-all") + parser_update.add_argument( + "configuration", help="Your YAML configuration file directory.", nargs=1 + ) return parser.parse_args(argv[1:]) @@ -596,9 +686,13 @@ def run_esphome(argv): CORE.dashboard = args.dashboard setup_log(args.verbose, args.quiet) - if args.command != "version" and not args.configuration: - _LOGGER.error("Missing configuration parameter, see esphome --help.") - return 1 + if args.deprecated_argv_suggestion is not None: + _LOGGER.warning( + "Calling ESPHome with the configuration before the command is deprecated " + "and will be removed in the future. " + ) + _LOGGER.warning("Please instead use:") + _LOGGER.warning(" esphome %s", " ".join(args.deprecated_argv_suggestion[1:])) if sys.version_info < (3, 7, 0): _LOGGER.error( diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index f0e9604141..4b40237768 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -62,7 +62,7 @@ class DashboardSettings: self.using_password = bool(password) if self.using_password: self.password_hash = password_hash(password) - self.config_dir = args.configuration[0] + self.config_dir = args.configuration @property def relative_url(self): @@ -274,9 +274,9 @@ class EsphomeLogsHandler(EsphomeCommandWebSocket): return [ "esphome", "--dashboard", - config_file, "logs", - "--serial-port", + config_file, + "--device", json_message["port"], ] @@ -287,9 +287,9 @@ class EsphomeUploadHandler(EsphomeCommandWebSocket): return [ "esphome", "--dashboard", - config_file, "run", - "--upload-port", + config_file, + "--device", json_message["port"], ] @@ -297,40 +297,40 @@ class EsphomeUploadHandler(EsphomeCommandWebSocket): class EsphomeCompileHandler(EsphomeCommandWebSocket): def build_command(self, json_message): config_file = settings.rel_path(json_message["configuration"]) - return ["esphome", "--dashboard", config_file, "compile"] + return ["esphome", "--dashboard", "compile", config_file] class EsphomeValidateHandler(EsphomeCommandWebSocket): def build_command(self, json_message): config_file = settings.rel_path(json_message["configuration"]) - return ["esphome", "--dashboard", config_file, "config"] + return ["esphome", "--dashboard", "config", config_file] class EsphomeCleanMqttHandler(EsphomeCommandWebSocket): def build_command(self, json_message): config_file = settings.rel_path(json_message["configuration"]) - return ["esphome", "--dashboard", config_file, "clean-mqtt"] + return ["esphome", "--dashboard", "clean-mqtt", config_file] class EsphomeCleanHandler(EsphomeCommandWebSocket): def build_command(self, json_message): config_file = settings.rel_path(json_message["configuration"]) - return ["esphome", "--dashboard", config_file, "clean"] + return ["esphome", "--dashboard", "clean", config_file] class EsphomeVscodeHandler(EsphomeCommandWebSocket): def build_command(self, json_message): - return ["esphome", "--dashboard", "-q", "dummy", "vscode"] + return ["esphome", "--dashboard", "-q", "vscode", "dummy"] class EsphomeAceEditorHandler(EsphomeCommandWebSocket): def build_command(self, json_message): - return ["esphome", "--dashboard", "-q", settings.config_dir, "vscode", "--ace"] + return ["esphome", "--dashboard", "-q", "vscode", settings.config_dir, "--ace"] class EsphomeUpdateAllHandler(EsphomeCommandWebSocket): def build_command(self, json_message): - return ["esphome", "--dashboard", settings.config_dir, "update-all"] + return ["esphome", "--dashboard", "update-all", settings.config_dir] class SerialPortRequestHandler(BaseHandler): diff --git a/script/test b/script/test index a6d99c8f62..0327e08ca8 100755 --- a/script/test +++ b/script/test @@ -6,7 +6,7 @@ cd "$(dirname "$0")/.." set -x -esphome tests/test1.yaml compile -esphome tests/test2.yaml compile -esphome tests/test3.yaml compile -esphome tests/test4.yaml compile +esphome compile tests/test1.yaml +esphome compile tests/test2.yaml +esphome compile tests/test3.yaml +esphome compile tests/test4.yaml