mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-22 11:43:51 +01: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.core import CORE, EsphomeError, coroutine, coroutine_with_priority | ||||||
| from esphome.helpers import color, indent | from esphome.helpers import color, indent | ||||||
| from esphome.py_compat import IS_PY2, safe_input | 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__) | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| @@ -187,8 +187,7 @@ def upload_program(config, args, host): | |||||||
|     ota_conf = config[CONF_OTA] |     ota_conf = config[CONF_OTA] | ||||||
|     remote_port = ota_conf[CONF_PORT] |     remote_port = ota_conf[CONF_PORT] | ||||||
|     password = ota_conf[CONF_PASSWORD] |     password = ota_conf[CONF_PASSWORD] | ||||||
|     res = espota2.run_ota(host, remote_port, password, CORE.firmware_bin) |     return espota2.run_ota(host, remote_port, password, CORE.firmware_bin) | ||||||
|     return res |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def show_logs(config, args, port): | def show_logs(config, args, port): | ||||||
| @@ -350,11 +349,52 @@ def command_dashboard(args): | |||||||
|     return dashboard.start_web_server(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 = { | PRE_CONFIG_ACTIONS = { | ||||||
|     'wizard': command_wizard, |     'wizard': command_wizard, | ||||||
|     'version': command_version, |     'version': command_version, | ||||||
|     'dashboard': command_dashboard, |     'dashboard': command_dashboard, | ||||||
|     'vscode': command_vscode, |     'vscode': command_vscode, | ||||||
|  |     'update-all': command_update_all, | ||||||
| } | } | ||||||
|  |  | ||||||
| POST_CONFIG_ACTIONS = { | POST_CONFIG_ACTIONS = { | ||||||
| @@ -446,6 +486,8 @@ def parse_args(argv): | |||||||
|     vscode = subparsers.add_parser('vscode', help=argparse.SUPPRESS) |     vscode = subparsers.add_parser('vscode', help=argparse.SUPPRESS) | ||||||
|     vscode.add_argument('--ace', action='store_true') |     vscode.add_argument('--ace', action='store_true') | ||||||
|  |  | ||||||
|  |     subparsers.add_parser('update-all', help=argparse.SUPPRESS) | ||||||
|  |  | ||||||
|     return parser.parse_args(argv[1:]) |     return parser.parse_args(argv[1:]) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -26,7 +26,7 @@ import tornado.process | |||||||
| import tornado.web | import tornado.web | ||||||
| import tornado.websocket | import tornado.websocket | ||||||
|  |  | ||||||
| from esphome import const | from esphome import const, util | ||||||
| from esphome.__main__ import get_serial_ports | from esphome.__main__ import get_serial_ports | ||||||
| from esphome.helpers import mkdir_p, get_bool_env, run_system_command | from esphome.helpers import mkdir_p, get_bool_env, run_system_command | ||||||
| from esphome.py_compat import IS_PY2, decode_text | from esphome.py_compat import IS_PY2, decode_text | ||||||
| @@ -93,17 +93,7 @@ class DashboardSettings(object): | |||||||
|         return os.path.join(self.config_dir, *args) |         return os.path.join(self.config_dir, *args) | ||||||
|  |  | ||||||
|     def list_yaml_files(self): |     def list_yaml_files(self): | ||||||
|         files = [] |         return util.list_yaml_files(self.config_dir) | ||||||
|         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 |  | ||||||
|  |  | ||||||
|  |  | ||||||
| settings = DashboardSettings() | settings = DashboardSettings() | ||||||
| @@ -122,6 +112,7 @@ def template_args(): | |||||||
|         'get_static_file_url': get_static_file_url, |         'get_static_file_url': get_static_file_url, | ||||||
|         'relative_url': settings.relative_url, |         'relative_url': settings.relative_url, | ||||||
|         'streamer_mode': get_bool_env('ESPHOME_STREAMER_MODE'), |         '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"] |         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): | class SerialPortRequestHandler(BaseHandler): | ||||||
|     @authenticated |     @authenticated | ||||||
|     def get(self): |     def get(self): | ||||||
| @@ -690,6 +686,7 @@ def make_app(debug=False): | |||||||
|         (rel + "clean", EsphomeCleanHandler), |         (rel + "clean", EsphomeCleanHandler), | ||||||
|         (rel + "vscode", EsphomeVscodeHandler), |         (rel + "vscode", EsphomeVscodeHandler), | ||||||
|         (rel + "ace", EsphomeAceEditorHandler), |         (rel + "ace", EsphomeAceEditorHandler), | ||||||
|  |         (rel + "update-all", EsphomeUpdateAllHandler), | ||||||
|         (rel + "edit", EditRequestHandler), |         (rel + "edit", EditRequestHandler), | ||||||
|         (rel + "download.bin", DownloadBinaryRequestHandler), |         (rel + "download.bin", DownloadBinaryRequestHandler), | ||||||
|         (rel + "serial-ports", SerialPortRequestHandler), |         (rel + "serial-ports", SerialPortRequestHandler), | ||||||
|   | |||||||
| @@ -131,10 +131,14 @@ ul.stepper:not(.horizontal) .step.active::before, ul.stepper:not(.horizontal) .s | |||||||
|  |  | ||||||
| .select-port-container { | .select-port-container { | ||||||
|   margin-top: 8px; |   margin-top: 8px; | ||||||
|   margin-right: 24px; |   margin-right: 10px; | ||||||
|   width: 350px; |   width: 350px; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #dropdown-nav-trigger { | ||||||
|  |   margin-right: 24px; | ||||||
|  | } | ||||||
|  |  | ||||||
| .select-port-container .select-dropdown { | .select-port-container .select-dropdown { | ||||||
|   color: #fff; |   color: #fff; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -333,6 +333,10 @@ class LogModalElem { | |||||||
|     this.activeSocket.close(); |     this.activeSocket.close(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   open(event) { | ||||||
|  |     this._onPress(event); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   _onPress(event) { |   _onPress(event) { | ||||||
|     this.activeConfig = event.target.getAttribute('data-node'); |     this.activeConfig = event.target.getAttribute('data-node'); | ||||||
|     this._setupModalInstance(); |     this._setupModalInstance(); | ||||||
| @@ -745,3 +749,32 @@ jQuery.validator.addMethod("nospaces", (value, element) => { | |||||||
| jQuery.validator.addMethod("lowercase", (value, element) => { | jQuery.validator.addMethod("lowercase", (value, element) => { | ||||||
|   return value === value.toLowerCase(); |   return value === value.toLowerCase(); | ||||||
| }, "Name must be lowercase."); | }, "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> | <nav> | ||||||
|   <div class="nav-wrapper indigo"> |   <div class="nav-wrapper indigo"> | ||||||
|     <a href="#" class="brand-logo left">ESPHome Dashboard</a> |     <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"> |     <div class="select-port-container right" id="select-port-target"> | ||||||
|       <select></select> |       <select></select> | ||||||
|     </div> |     </div> | ||||||
|   </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> | </nav> | ||||||
|  |  | ||||||
| {% if begin %} | {% if begin %} | ||||||
| @@ -445,6 +451,16 @@ | |||||||
|   </div> |   </div> | ||||||
| </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"> | <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> |   <i class="material-icons">add</i> | ||||||
| </a> | </a> | ||||||
|   | |||||||
| @@ -299,3 +299,4 @@ def run_ota(remote_host, remote_port, password, filename): | |||||||
|         return run_ota_impl_(remote_host, remote_port, password, filename) |         return run_ota_impl_(remote_host, remote_port, password, filename) | ||||||
|     except OTAError as err: |     except OTAError as err: | ||||||
|         _LOGGER.error(err) |         _LOGGER.error(err) | ||||||
|  |         return 1 | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ from __future__ import print_function | |||||||
| import collections | import collections | ||||||
| import io | import io | ||||||
| import logging | import logging | ||||||
|  | import os | ||||||
| import re | import re | ||||||
| import subprocess | import subprocess | ||||||
| import sys | import sys | ||||||
| @@ -207,3 +208,16 @@ class OrderedDict(collections.OrderedDict): | |||||||
|                 root[1] = first[0] = link |                 root[1] = first[0] = link | ||||||
|         else: |         else: | ||||||
|             super(OrderedDict, self).move_to_end(key, last=last)  # pylint: disable=no-member |             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.config_helpers import read_config_file | ||||||
| from esphome.core import EsphomeError, IPAddress, Lambda, MACAddress, TimePeriod, DocumentRange | from esphome.core import EsphomeError, IPAddress, Lambda, MACAddress, TimePeriod, DocumentRange | ||||||
| from esphome.py_compat import text_type, IS_PY2 | 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__) | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| @@ -260,12 +260,12 @@ class ESPHomeLoader(yaml.SafeLoader):  # pylint: disable=too-many-ancestors | |||||||
|  |  | ||||||
|     @_add_data_ref |     @_add_data_ref | ||||||
|     def construct_include_dir_list(self, node): |     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] |         return [_load_yaml_internal(f) for f in files] | ||||||
|  |  | ||||||
|     @_add_data_ref |     @_add_data_ref | ||||||
|     def construct_include_dir_merge_list(self, node): |     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 = [] |         merged_list = [] | ||||||
|         for fname in files: |         for fname in files: | ||||||
|             loaded_yaml = _load_yaml_internal(fname) |             loaded_yaml = _load_yaml_internal(fname) | ||||||
| @@ -275,7 +275,7 @@ class ESPHomeLoader(yaml.SafeLoader):  # pylint: disable=too-many-ancestors | |||||||
|  |  | ||||||
|     @_add_data_ref |     @_add_data_ref | ||||||
|     def construct_include_dir_named(self, node): |     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() |         mapping = OrderedDict() | ||||||
|         for fname in files: |         for fname in files: | ||||||
|             filename = os.path.splitext(os.path.basename(fname))[0] |             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 |     @_add_data_ref | ||||||
|     def construct_include_dir_merge_named(self, node): |     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() |         mapping = OrderedDict() | ||||||
|         for fname in files: |         for fname in files: | ||||||
|             loaded_yaml = _load_yaml_internal(fname) |             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)) |         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: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:float', ESPHomeLoader.construct_yaml_float) | ||||||
| ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:binary', ESPHomeLoader.construct_yaml_binary) | ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:binary', ESPHomeLoader.construct_yaml_binary) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user