mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	| @@ -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:]) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -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), | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
| }); | ||||
|   | ||||
| @@ -31,10 +31,16 @@ | ||||
| <nav> | ||||
|   <div class="nav-wrapper indigo"> | ||||
|     <a href="#" class="brand-logo left">ESPHome Dashboard</a> | ||||
|     <i class="material-icons dropdown-trigger right" id="dropdown-nav-trigger" data-target="dropdown-nav-actions">more_vert</i> | ||||
|     <div class="select-port-container right" id="select-port-target"> | ||||
|       <select></select> | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
|   <ul id="dropdown-nav-actions" class="select-action dropdown-content card-dropdown-action"> | ||||
|     <li><a id="update-all-button" class="modal-close waves-effect waves-green btn-flat" | ||||
|            data-node="{{ escape(config_dir) }}">Update All</a></li> | ||||
|   </ul> | ||||
| </nav> | ||||
|  | ||||
| {% if begin %} | ||||
| @@ -445,6 +451,16 @@ | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| <div id="modal-update-all" class="modal modal-fixed-footer"> | ||||
|   <div class="modal-content"> | ||||
|     <h4>Update All</h4> | ||||
|     <pre class="log"></pre> | ||||
|   </div> | ||||
|   <div class="modal-footer"> | ||||
|     <a class="modal-close waves-effect waves-green btn-flat stop-logs">Stop</a> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| <a class="btn-floating btn-large ribbon-fab waves-effect waves-light pink accent-2" id="setup-wizard-start"> | ||||
|   <i class="material-icons">add</i> | ||||
| </a> | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user