From 7a895adec9dcf49c245ea2cfabab461d348ba2d7 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 7 Jun 2019 14:26:28 +0200 Subject: [PATCH] Dashboard Update all button (#615) * Add update all button * Use bold --- esphome/__main__.py | 48 ++++++++++++++++++++++++-- esphome/dashboard/dashboard.py | 21 +++++------ esphome/dashboard/static/esphome.css | 6 +++- esphome/dashboard/static/esphome.js | 33 ++++++++++++++++++ esphome/dashboard/templates/index.html | 16 +++++++++ esphome/espota2.py | 1 + esphome/util.py | 14 ++++++++ esphome/yaml_util.py | 16 +++------ 8 files changed, 128 insertions(+), 27 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 8b1815141b..f166cfcd87 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -15,7 +15,7 @@ from esphome.const import CONF_BAUD_RATE, CONF_BROKER, CONF_LOGGER, CONF_OTA, \ from esphome.core import CORE, EsphomeError, coroutine, coroutine_with_priority from esphome.helpers import color, indent from esphome.py_compat import IS_PY2, safe_input -from esphome.util import run_external_command, run_external_process, safe_print +from esphome.util import run_external_command, run_external_process, safe_print, list_yaml_files _LOGGER = logging.getLogger(__name__) @@ -187,8 +187,7 @@ def upload_program(config, args, host): ota_conf = config[CONF_OTA] remote_port = ota_conf[CONF_PORT] password = ota_conf[CONF_PASSWORD] - res = espota2.run_ota(host, remote_port, password, CORE.firmware_bin) - return res + return espota2.run_ota(host, remote_port, password, CORE.firmware_bin) def show_logs(config, args, port): @@ -350,11 +349,52 @@ def command_dashboard(args): return dashboard.start_web_server(args) +def command_update_all(args): + import click + + success = {} + files = list_yaml_files(args.configuration) + twidth = 60 + + def print_bar(middle_text): + middle_text = " {} ".format(middle_text) + width = len(click.unstyle(middle_text)) + half_line = "=" * ((twidth - width) / 2) + click.echo("%s%s%s" % (half_line, middle_text, half_line)) + + for f in files: + print("Updating {}".format(color('cyan', f))) + print('-' * twidth) + print() + rc = run_external_process('esphome', f, 'run', '--no-logs') + if rc == 0: + print_bar("[{}] {}".format(color('bold_green', 'SUCCESS'), f)) + success[f] = True + else: + print_bar("[{}] {}".format(color('bold_red', 'ERROR'), f)) + success[f] = False + + print() + print() + print() + + print_bar('[{}]'.format(color('bold_white', 'SUMMARY'))) + failed = 0 + for f in files: + if success[f]: + print(" - {}: {}".format(f, color('green', 'SUCCESS'))) + else: + print(" - {}: {}".format(f, color('bold_red', 'FAILED'))) + failed += 1 + return failed + + PRE_CONFIG_ACTIONS = { 'wizard': command_wizard, 'version': command_version, 'dashboard': command_dashboard, 'vscode': command_vscode, + 'update-all': command_update_all, } POST_CONFIG_ACTIONS = { @@ -446,6 +486,8 @@ def parse_args(argv): vscode = subparsers.add_parser('vscode', help=argparse.SUPPRESS) vscode.add_argument('--ace', action='store_true') + subparsers.add_parser('update-all', help=argparse.SUPPRESS) + return parser.parse_args(argv[1:]) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 4df7b5a386..2302f7730b 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -26,7 +26,7 @@ import tornado.process import tornado.web import tornado.websocket -from esphome import const +from esphome import const, util from esphome.__main__ import get_serial_ports from esphome.helpers import mkdir_p, get_bool_env, run_system_command from esphome.py_compat import IS_PY2, decode_text @@ -93,17 +93,7 @@ class DashboardSettings(object): return os.path.join(self.config_dir, *args) def list_yaml_files(self): - files = [] - for file in os.listdir(self.config_dir): - if not file.endswith('.yaml'): - continue - if file.startswith('.'): - continue - if file == 'secrets.yaml': - continue - files.append(file) - files.sort() - return files + return util.list_yaml_files(self.config_dir) settings = DashboardSettings() @@ -122,6 +112,7 @@ def template_args(): 'get_static_file_url': get_static_file_url, 'relative_url': settings.relative_url, 'streamer_mode': get_bool_env('ESPHOME_STREAMER_MODE'), + 'config_dir': settings.config_dir, } @@ -315,6 +306,11 @@ class EsphomeAceEditorHandler(EsphomeCommandWebSocket): return ["esphome", "--dashboard", "-q", settings.config_dir, "vscode", "--ace"] +class EsphomeUpdateAllHandler(EsphomeCommandWebSocket): + def build_command(self, json_message): + return ["esphome", "--dashboard", settings.config_dir, "update-all"] + + class SerialPortRequestHandler(BaseHandler): @authenticated def get(self): @@ -690,6 +686,7 @@ def make_app(debug=False): (rel + "clean", EsphomeCleanHandler), (rel + "vscode", EsphomeVscodeHandler), (rel + "ace", EsphomeAceEditorHandler), + (rel + "update-all", EsphomeUpdateAllHandler), (rel + "edit", EditRequestHandler), (rel + "download.bin", DownloadBinaryRequestHandler), (rel + "serial-ports", SerialPortRequestHandler), diff --git a/esphome/dashboard/static/esphome.css b/esphome/dashboard/static/esphome.css index 29455d9851..5300e73861 100644 --- a/esphome/dashboard/static/esphome.css +++ b/esphome/dashboard/static/esphome.css @@ -131,10 +131,14 @@ ul.stepper:not(.horizontal) .step.active::before, ul.stepper:not(.horizontal) .s .select-port-container { margin-top: 8px; - margin-right: 24px; + margin-right: 10px; width: 350px; } +#dropdown-nav-trigger { + margin-right: 24px; +} + .select-port-container .select-dropdown { color: #fff; } diff --git a/esphome/dashboard/static/esphome.js b/esphome/dashboard/static/esphome.js index 1cf3dde2d6..2fb61be9cc 100644 --- a/esphome/dashboard/static/esphome.js +++ b/esphome/dashboard/static/esphome.js @@ -333,6 +333,10 @@ class LogModalElem { this.activeSocket.close(); } + open(event) { + this._onPress(event); + } + _onPress(event) { this.activeConfig = event.target.getAttribute('data-node'); this._setupModalInstance(); @@ -745,3 +749,32 @@ jQuery.validator.addMethod("nospaces", (value, element) => { jQuery.validator.addMethod("lowercase", (value, element) => { return value === value.toLowerCase(); }, "Name must be lowercase."); + + + +const updateAllModal = new LogModalElem({ + name: 'update-all', + onPrepare: (modalElem, config) => { + modalElem.querySelector('.stop-logs').innerHTML = "Stop"; + downloadButton.classList.add('disabled'); + }, + onProcessExit: (modalElem, code) => { + if (code === 0) { + M.toast({html: "Program exited successfully."}); + downloadButton.classList.remove('disabled'); + } else { + M.toast({html: `Program failed with code ${data.code}`}); + } + modalElem.querySelector(".stop-logs").innerHTML = "Close"; + }, + onSocketClose: (modalElem) => { + M.toast({html: 'Terminated process.'}); + }, + dismissible: false, +}); +updateAllModal.setup(); + +const updateAllButton = document.getElementById('update-all-button'); +updateAllButton.addEventListener('click', (e) => { + updateAllModal.open(e); +}); diff --git a/esphome/dashboard/templates/index.html b/esphome/dashboard/templates/index.html index 1929f42397..85a03457ac 100644 --- a/esphome/dashboard/templates/index.html +++ b/esphome/dashboard/templates/index.html @@ -31,10 +31,16 @@ {% if begin %} @@ -445,6 +451,16 @@ + + add diff --git a/esphome/espota2.py b/esphome/espota2.py index 786f49dbdf..e50f3a4eb7 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -299,3 +299,4 @@ def run_ota(remote_host, remote_port, password, filename): return run_ota_impl_(remote_host, remote_port, password, filename) except OTAError as err: _LOGGER.error(err) + return 1 diff --git a/esphome/util.py b/esphome/util.py index 0ae0b9e32e..f631e48a6b 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -3,6 +3,7 @@ from __future__ import print_function import collections import io import logging +import os import re import subprocess import sys @@ -207,3 +208,16 @@ class OrderedDict(collections.OrderedDict): root[1] = first[0] = link else: super(OrderedDict, self).move_to_end(key, last=last) # pylint: disable=no-member + + +def list_yaml_files(folder): + files = filter_yaml_files([os.path.join(folder, p) for p in os.listdir(folder)]) + files.sort() + return files + + +def filter_yaml_files(files): + files = [f for f in files if os.path.splitext(f)[1] == '.yaml'] + files = [f for f in files if os.path.basename(f) != 'secrets.yaml'] + files = [f for f in files if not os.path.basename(f).startswith('.')] + return files diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 30e8bb82de..fb04d7d5b0 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -14,7 +14,7 @@ from esphome import core from esphome.config_helpers import read_config_file from esphome.core import EsphomeError, IPAddress, Lambda, MACAddress, TimePeriod, DocumentRange from esphome.py_compat import text_type, IS_PY2 -from esphome.util import OrderedDict +from esphome.util import OrderedDict, filter_yaml_files _LOGGER = logging.getLogger(__name__) @@ -260,12 +260,12 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors @_add_data_ref def construct_include_dir_list(self, node): - files = _filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) + files = filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) return [_load_yaml_internal(f) for f in files] @_add_data_ref def construct_include_dir_merge_list(self, node): - files = _filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) + files = filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) merged_list = [] for fname in files: loaded_yaml = _load_yaml_internal(fname) @@ -275,7 +275,7 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors @_add_data_ref def construct_include_dir_named(self, node): - files = _filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) + files = filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) mapping = OrderedDict() for fname in files: filename = os.path.splitext(os.path.basename(fname))[0] @@ -284,7 +284,7 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors @_add_data_ref def construct_include_dir_merge_named(self, node): - files = _filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) + files = filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) mapping = OrderedDict() for fname in files: loaded_yaml = _load_yaml_internal(fname) @@ -297,12 +297,6 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors return Lambda(text_type(node.value)) -def _filter_yaml_files(files): - files = [f for f in files if os.path.basename(f) != SECRET_YAML] - files = [f for f in files if not os.path.basename(f).startswith('.')] - return files - - ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:int', ESPHomeLoader.construct_yaml_int) ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:float', ESPHomeLoader.construct_yaml_float) ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:binary', ESPHomeLoader.construct_yaml_binary)