mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	HassIO -> dashboard
This commit is contained in:
		
							
								
								
									
										0
									
								
								esphomeyaml/dashboard/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphomeyaml/dashboard/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										176
									
								
								esphomeyaml/dashboard/dashboard.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								esphomeyaml/dashboard/dashboard.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,176 @@ | ||||
| from __future__ import print_function | ||||
|  | ||||
| import codecs | ||||
| import json | ||||
| import logging | ||||
| import os | ||||
| import random | ||||
| import subprocess | ||||
|  | ||||
| try: | ||||
|     import tornado | ||||
|     import tornado.gen | ||||
|     import tornado.ioloop | ||||
|     import tornado.iostream | ||||
|     import tornado.process | ||||
|     import tornado.web | ||||
|     import tornado.websocket | ||||
|     import tornado.concurrent | ||||
| except ImportError as err: | ||||
|     pass | ||||
|  | ||||
| from esphomeyaml import const, core, __main__ | ||||
| from esphomeyaml.__main__ import get_serial_ports, get_base_path, get_name | ||||
| from esphomeyaml.helpers import quote | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
| CONFIG_DIR = '' | ||||
|  | ||||
| # pylint: disable=abstract-method, arguments-differ | ||||
| class EsphomeyamlCommandWebSocket(tornado.websocket.WebSocketHandler): | ||||
|     def __init__(self, application, request, **kwargs): | ||||
|         super(EsphomeyamlCommandWebSocket, self).__init__(application, request, **kwargs) | ||||
|         self.proc = None | ||||
|         self.closed = False | ||||
|  | ||||
|     def on_message(self, message): | ||||
|         if self.proc is not None: | ||||
|             return | ||||
|         command = self.build_command(message) | ||||
|         _LOGGER.debug(u"WebSocket opened for command %s", [quote(x) for x in command]) | ||||
|         self.proc = tornado.process.Subprocess(command, | ||||
|                                                stdout=tornado.process.Subprocess.STREAM, | ||||
|                                                stderr=subprocess.STDOUT) | ||||
|         self.proc.set_exit_callback(self.proc_on_exit) | ||||
|         tornado.ioloop.IOLoop.current().spawn_callback(self.redirect_stream) | ||||
|  | ||||
|     @tornado.gen.coroutine | ||||
|     def redirect_stream(self): | ||||
|         while True: | ||||
|             try: | ||||
|                 data = yield self.proc.stdout.read_until_regex('[\n\r]') | ||||
|             except tornado.iostream.StreamClosedError: | ||||
|                 break | ||||
|             if data.endswith('\r') and random.randrange(100) < 90: | ||||
|                 continue | ||||
|             data = data.replace('\033', '\\033') | ||||
|             self.write_message({'event': 'line', 'data': data}) | ||||
|  | ||||
|     def proc_on_exit(self, returncode): | ||||
|         if not self.closed: | ||||
|             _LOGGER.debug("Process exited with return code %s", returncode) | ||||
|             self.write_message({'event': 'exit', 'code': returncode}) | ||||
|  | ||||
|     def on_close(self): | ||||
|         self.closed = True | ||||
|         if self.proc is not None and self.proc.returncode is None: | ||||
|             _LOGGER.debug("Terminating process") | ||||
|             self.proc.proc.terminate() | ||||
|  | ||||
|     def build_command(self, message): | ||||
|         raise NotImplementedError | ||||
|  | ||||
|  | ||||
| class EsphomeyamlLogsHandler(EsphomeyamlCommandWebSocket): | ||||
|     def build_command(self, message): | ||||
|         js = json.loads(message) | ||||
|         config_file = CONFIG_DIR + '/' + js['configuration'] | ||||
|         return ["esphomeyaml", config_file, "logs", '--serial-port', js["port"], '--escape'] | ||||
|  | ||||
|  | ||||
| class EsphomeyamlRunHandler(EsphomeyamlCommandWebSocket): | ||||
|     def build_command(self, message): | ||||
|         js = json.loads(message) | ||||
|         config_file = os.path.join(CONFIG_DIR, js['configuration']) | ||||
|         return ["esphomeyaml", config_file, "run", '--upload-port', js["port"], | ||||
|                 '--escape', '--use-esptoolpy'] | ||||
|  | ||||
|  | ||||
| class EsphomeyamlCompileHandler(EsphomeyamlCommandWebSocket): | ||||
|     def build_command(self, message): | ||||
|         js = json.loads(message) | ||||
|         config_file = os.path.join(CONFIG_DIR, js['configuration']) | ||||
|         return ["esphomeyaml", config_file, "compile"] | ||||
|  | ||||
|  | ||||
| class SerialPortRequestHandler(tornado.web.RequestHandler): | ||||
|     def get(self): | ||||
|         ports = get_serial_ports() | ||||
|         data = [] | ||||
|         for port, desc in ports: | ||||
|             if port == '/dev/ttyAMA0': | ||||
|                 # ignore RPi built-in serial port | ||||
|                 continue | ||||
|             data.append({'port': port, 'desc': desc}) | ||||
|         data.append({'port': 'OTA', 'desc': 'Over-The-Air Upload/Logs'}) | ||||
|         self.write(json.dumps(data)) | ||||
|  | ||||
|  | ||||
| class WizardRequestHandler(tornado.web.RequestHandler): | ||||
|     def post(self): | ||||
|         from esphomeyaml import wizard | ||||
|  | ||||
|         kwargs = {k: ''.join(v) for k, v in self.request.arguments.iteritems()} | ||||
|         config = wizard.wizard_file(**kwargs) | ||||
|         destination = os.path.join(CONFIG_DIR, kwargs['name'] + '.yaml') | ||||
|         with codecs.open(destination, 'w') as f_handle: | ||||
|             f_handle.write(config) | ||||
|  | ||||
|         self.redirect('/') | ||||
|  | ||||
|  | ||||
| class DownloadBinaryRequestHandler(tornado.web.RequestHandler): | ||||
|     def get(self): | ||||
|         configuration = self.get_argument('configuration') | ||||
|         config_file = os.path.join(CONFIG_DIR, configuration) | ||||
|         core.CONFIG_PATH = config_file | ||||
|         config = __main__.read_config(core.CONFIG_PATH) | ||||
|         name = get_name(config) | ||||
|         path = os.path.join(get_base_path(config), '.pioenvs', name, 'firmware.bin') | ||||
|         self.set_header('Content-Type', 'application/octet-stream') | ||||
|         self.set_header("Content-Disposition", 'attachment; filename="{}.bin"'.format(name)) | ||||
|         with open(path, 'rb') as f: | ||||
|             while 1: | ||||
|                 data = f.read(16384)  # or some other nice-sized chunk | ||||
|                 if not data: | ||||
|                     break | ||||
|                 self.write(data) | ||||
|         self.finish() | ||||
|  | ||||
|  | ||||
| class MainRequestHandler(tornado.web.RequestHandler): | ||||
|     def get(self): | ||||
|         files = sorted([f for f in os.listdir(CONFIG_DIR) if f.endswith('.yaml')]) | ||||
|         full_path_files = [os.path.join(CONFIG_DIR, f) for f in files] | ||||
|         self.render("templates/index.html", files=files, full_path_files=full_path_files, | ||||
|                     version=const.__version__) | ||||
|  | ||||
|  | ||||
| def make_app(): | ||||
|     static_path = os.path.join(os.path.dirname(__file__), 'static') | ||||
|     return tornado.web.Application([ | ||||
|         (r"/", MainRequestHandler), | ||||
|         (r"/logs", EsphomeyamlLogsHandler), | ||||
|         (r"/run", EsphomeyamlRunHandler), | ||||
|         (r"/compile", EsphomeyamlCompileHandler), | ||||
|         (r"/download.bin", DownloadBinaryRequestHandler), | ||||
|         (r"/serial-ports", SerialPortRequestHandler), | ||||
|         (r"/wizard.html", WizardRequestHandler), | ||||
|         (r'/static/(.*)', tornado.web.StaticFileHandler, {'path': static_path}), | ||||
|     ], debug=False) | ||||
|  | ||||
|  | ||||
| def start_web_server(args): | ||||
|     global CONFIG_DIR | ||||
|     CONFIG_DIR = args.configuration | ||||
|     if not os.path.exists(CONFIG_DIR): | ||||
|         os.makedirs(CONFIG_DIR) | ||||
|  | ||||
|     _LOGGER.info("Starting dashboard web server on port %s and configuration dir %s...", | ||||
|                  args.port, CONFIG_DIR) | ||||
|     app = make_app() | ||||
|     app.listen(args.port) | ||||
|     try: | ||||
|         tornado.ioloop.IOLoop.current().start() | ||||
|     except KeyboardInterrupt: | ||||
|         _LOGGER.info("Shutting down...") | ||||
							
								
								
									
										5
									
								
								esphomeyaml/dashboard/static/materialize-stepper.min.css
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										5
									
								
								esphomeyaml/dashboard/static/materialize-stepper.min.css
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										5
									
								
								esphomeyaml/dashboard/static/materialize-stepper.min.js
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										5
									
								
								esphomeyaml/dashboard/static/materialize-stepper.min.js
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										741
									
								
								esphomeyaml/dashboard/templates/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										741
									
								
								esphomeyaml/dashboard/templates/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,741 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|   <meta charset="UTF-8"> | ||||
|   <title>esphomeyaml Dashboard</title> | ||||
|   <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> | ||||
|   <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/css/materialize.min.css"> | ||||
|  | ||||
|   <link rel="stylesheet" href="/static/materialize-stepper.min.css"> | ||||
|  | ||||
|   <!-- jQuery :( --> | ||||
|   <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script> | ||||
|   <script src="https://code.jquery.com/ui/1.8.5/jquery-ui.min.js" integrity="sha256-fOse6WapxTrUSJOJICXXYwHRJOPa6C1OUQXi7C9Ddy8=" crossorigin="anonymous"></script> | ||||
|   <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/js/materialize.min.js"></script> | ||||
|   <script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.15.0/jquery.validate.min.js"></script> | ||||
|  | ||||
|  | ||||
|   <script src="/static/materialize-stepper.min.js"></script> | ||||
|  | ||||
|   <style> | ||||
|     nav .brand-logo { | ||||
|       margin-left: 48px; | ||||
|       font-size: 20px; | ||||
|     } | ||||
|  | ||||
|     main .container { | ||||
|       margin-top: -12vh; | ||||
|       flex-shrink: 0; | ||||
|     } | ||||
|  | ||||
|     .ribbon { | ||||
|       width: 100%; | ||||
|       height: 17vh; | ||||
|       background-color: #3F51B5; | ||||
|       flex-shrink: 0; | ||||
|     } | ||||
|  | ||||
|     .ribbon-fab:not(.tap-target-origin) { | ||||
|       position: absolute; | ||||
|       right: 24px; | ||||
|       top: calc(17vh + 34px); | ||||
|     } | ||||
|  | ||||
|     i.very-large { | ||||
|       font-size: 8rem; | ||||
|       padding-top: 2px; | ||||
|       color: #424242; | ||||
|     } | ||||
|  | ||||
|     .card .card-content { | ||||
|       padding-left: 18px; | ||||
|       padding-bottom: 10px; | ||||
|     } | ||||
|  | ||||
|     .chip { | ||||
|       height: 26px; | ||||
|       font-size: 12px; | ||||
|       line-height: 26px; | ||||
|     } | ||||
|  | ||||
|     .log { | ||||
|       background-color: #1c1c1c; | ||||
|       margin-top: 0; | ||||
|       margin-bottom: 0; | ||||
|       font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; | ||||
|       font-size: 12px; | ||||
|       padding: 16px; | ||||
|       overflow: auto; | ||||
|       line-height: 1.45; | ||||
|       border-radius: 3px; | ||||
|       word-wrap: normal; | ||||
|       color: #DDD; | ||||
|     } | ||||
|  | ||||
|     .inlinecode { | ||||
|       box-sizing: border-box; | ||||
|       padding: 0.2em 0.4em; | ||||
|       margin: 0; | ||||
|       font-size: 85%; | ||||
|       background-color: rgba(27,31,35,0.05); | ||||
|       border-radius: 3px; | ||||
|       font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace; | ||||
|     } | ||||
|  | ||||
|     .log.bold { | ||||
|       font-weight: bold; | ||||
|     } | ||||
|  | ||||
|     .log .v { | ||||
|       color: #888888; | ||||
|     } | ||||
|  | ||||
|     .log .d { | ||||
|       color: #00DDDD; | ||||
|     } | ||||
|  | ||||
|     .log .c { | ||||
|       color: magenta; | ||||
|     } | ||||
|  | ||||
|     .log .i { | ||||
|       color: limegreen; | ||||
|     } | ||||
|  | ||||
|     .log .w { | ||||
|       color: yellow; | ||||
|     } | ||||
|  | ||||
|     .log .e { | ||||
|       color: red; | ||||
|       font-weight: bold; | ||||
|     } | ||||
|  | ||||
|     .log .e { | ||||
|       color: red; | ||||
|     } | ||||
|  | ||||
|     .log .ww { | ||||
|       color: white; | ||||
|     } | ||||
|  | ||||
|     .modal { | ||||
|       width: 90%; | ||||
|       max-height: 85%; | ||||
|       height: 80% !important; | ||||
|     } | ||||
|  | ||||
|     .log { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     .page-footer { | ||||
|       padding-top: 0; | ||||
|     } | ||||
|  | ||||
|     body { | ||||
|       display: flex; | ||||
|       min-height: 100vh; | ||||
|       flex-direction: column; | ||||
|     } | ||||
|  | ||||
|     main { | ||||
|       flex: 1 0 auto; | ||||
|     } | ||||
|  | ||||
|     ul.browser-default { | ||||
|       padding-left: 30px; | ||||
|       margin-top: 10px; | ||||
|       margin-bottom: 15px; | ||||
|     } | ||||
|  | ||||
|     ul.browser-default li { | ||||
|       list-style-type: initial; | ||||
|     } | ||||
|  | ||||
|     ul.stepper:not(.horizontal) .step.active::before, ul.stepper:not(.horizontal) .step.done::before, ul.stepper.horizontal .step.active .step-title::before, ul.stepper.horizontal .step.done .step-title::before { | ||||
|       background-color: #3f51b5 !important; | ||||
|     } | ||||
|  | ||||
|     .select-port-container { | ||||
|       margin-top: 19px; | ||||
|     } | ||||
|   </style> | ||||
| </head> | ||||
| <body> | ||||
|  | ||||
| <header> | ||||
| <nav> | ||||
|   <div class="nav-wrapper indigo"> | ||||
|     <a href="#" class="brand-logo left">esphomeyaml Dashboard</a> | ||||
|   </div> | ||||
| </nav> | ||||
|  | ||||
| <div class="ribbon"></div> | ||||
| </header> | ||||
|  | ||||
| <main> | ||||
| <div class="container"> | ||||
|   {% for file, full_path in zip(files, full_path_files) %} | ||||
|   <div class="row"> | ||||
|     <div class="col s8 offset-s2 m10 offset-m1 l12"> | ||||
|       <div class="card horizontal"> | ||||
|         <div class="card-image center-align"> | ||||
|           <i class="material-icons very-large icon-grey">memory</i> | ||||
|         </div> | ||||
|         <div class="card-stacked"> | ||||
|           <div class="card-content"> | ||||
|             <span class="card-title">{{ escape(file) }}</span> | ||||
|             <p> | ||||
|               Full path: <code class="inlinecode">{{ escape(full_path) }}</code> | ||||
|             </p> | ||||
|           </div> | ||||
|           <div class="card-action"> | ||||
|             <a href="#" class="action-upload" data-node="{{ file }}">Upload</a> | ||||
|             <a href="#" class="action-compile" data-node="{{ file }}">Compile</a> | ||||
|             <a href="#" class="action-show-logs" data-node="{{ file }}">Show Logs</a> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|   {% end %} | ||||
| </div> | ||||
|  | ||||
| <div id="modal-logs" class="modal modal-fixed-footer"> | ||||
|   <div class="modal-content"> | ||||
|     <h4>Show Logs</h4> | ||||
|     <div class="upload-port row"> | ||||
|       <div class="col s12"> | ||||
|         <h5>Found multiple serial ports, please choose one:</h5> | ||||
|       </div> | ||||
|       <div class="input-field col s8"> | ||||
|         <select></select> | ||||
|       </div> | ||||
|       <div class="col s4 select-port-container"> | ||||
|         <button class="btn waves-effect waves-light upload-port-submit" type="submit" name="action">Select | ||||
|           <i class="material-icons right">send</i> | ||||
|         </button> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="log-container"> | ||||
|       <pre class="log"></pre> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="modal-footer"> | ||||
|     <a class="modal-close waves-effect waves-green btn-flat">Close</a> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| <div id="modal-upload" class="modal modal-fixed-footer"> | ||||
|   <div class="modal-content"> | ||||
|     <h4>Compile And Upload</h4> | ||||
|     <div class="upload-port row"> | ||||
|       <div class="col s12"> | ||||
|         <h5>Found multiple upload options, please choose one:</h5> | ||||
|       </div> | ||||
|       <div class="input-field col s8"> | ||||
|         <select></select> | ||||
|       </div> | ||||
|       <div class="col s4 select-port-container"> | ||||
|         <button class="btn waves-effect waves-light upload-port-submit" type="submit" name="action">Select | ||||
|           <i class="material-icons right">send</i> | ||||
|         </button> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="log-container"> | ||||
|       <pre class="log"></pre> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="modal-footer"> | ||||
|     <a class="modal-close waves-effect waves-green btn-flat">Stop</a> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| <div id="modal-compile" class="modal modal-fixed-footer"> | ||||
|   <div class="modal-content"> | ||||
|     <h4>Compile</h4> | ||||
|     <div class="log-container"> | ||||
|       <pre class="log"></pre> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="modal-footer"> | ||||
|     <a class="modal-close waves-effect waves-green btn-flat disabled download-binary">Download Binary</a> | ||||
|     <a class="modal-close waves-effect waves-green btn-flat">Stop</a> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| <div id="modal-wizard" class="modal"> | ||||
|   <div class="modal-content"> | ||||
|     <form action="/wizard.html" method="POST"> | ||||
|     <ul class="stepper linear"> | ||||
|       <li class="step active"> | ||||
|         <div class="step-title waves-effect">Introduction And Name</div> | ||||
|         <div class="step-content"> | ||||
|           <div class="row"> | ||||
|             <p> | ||||
|               Hi there! I'm the esphomeyaml setup wizard and will guide you through setting up | ||||
|               your first ESP8266 or ESP32-powered device using esphomeyaml. | ||||
|             </p> | ||||
|               <a href="https://www.espressif.com/en/products/hardware/esp8266ex/overview" target="_blank">ESP8266s</a> and | ||||
|               their successors (the <a href="https://www.espressif.com/en/products/hardware/esp32/overview" target="_blank">ESP32s</a>) | ||||
|               are great low-cost microcontrollers that can communicate with the outside world using WiFi. | ||||
|               They're found in many devices such as the popular Sonoff/iTead, but also exist as development boards | ||||
|               such as the <a href="http://nodemcu.com/index_en.html" target="_blank">NodeMCU</a>. | ||||
|             <p> | ||||
|             </p> | ||||
|               <a href="https://esphomelib.com/esphomeyaml/index.html" target="_blank">esphomeyaml</a>, | ||||
|               the tool you're using here, creates custom firmwares for these devices using YAML configuration | ||||
|               files (similar to the ones you might be used to with Home Assistant). | ||||
|             <p> | ||||
|             </p> | ||||
|               This wizard will create a basic YAML configuration file for your "node" (the microcontroller). | ||||
|               Later, you will be able to customize this file and add some of | ||||
|               <a href="https://github.com/OttoWinter/esphomelib" target="_blank">esphomelib's</a> | ||||
|               many integrations. | ||||
|             <p> | ||||
|             <p> | ||||
|               First, I need to know what this node should be called. Choose this name wisely, changing this | ||||
|               later makes Over-The-Air Update attempts difficult. | ||||
|               It may only contain the characters <code class="inlinecode">a-z</code>, | ||||
|               <code class="inlinecode">0-9</code> and <code class="inlinecode">_</code> | ||||
|             </p> | ||||
|             <div class="input-field col s12"> | ||||
|               <input id="node_name" class="validate" type="text" name="name" required> | ||||
|               <label for="node_name">Name of node</label> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="step-actions"> | ||||
|             <button class="waves-effect waves-dark btn indigo next-step"">CONTINUE</button> | ||||
|           </div> | ||||
|         </div> | ||||
|       </li> | ||||
|       <li class="step"> | ||||
|         <div class="step-title waves-effect">Device Type</div> | ||||
|         <div class="step-content"> | ||||
|           <div class="row"> | ||||
|             <p> | ||||
|               Great! Now I need to know what type of microcontroller you're using so that I can compile firmware for them. | ||||
|               Please choose either ESP32 or ESP8266 (use ESP8266 for Sonoff devices). Note that the ESP32 is currently | ||||
|               unsupported if HassIO is running on a Raspberry Pi. | ||||
|             </p> | ||||
|             <div class="input-field col s12"> | ||||
|               <select id="esp_type" name="platform" required> | ||||
|                 <option value="ESP8266">ESP8266</option> | ||||
|                 <option value="ESP32">ESP32</option> | ||||
|               </select> | ||||
|               <label>Microcontroller Type</label> | ||||
|             </div> | ||||
|             <p> | ||||
|               I'm also going to need to know which type of board you're using. Please go to | ||||
|               <a href="http://docs.platformio.org/en/latest/platforms/espressif32.html#boards" target="_blank">ESP32 boards</a> or | ||||
|               <a href="http://docs.platformio.org/en/latest/platforms/espressif8266.html#boards" target="_blank">ESP8266 boards</a>, | ||||
|               find your board and enter it here. For example, enter <code class="inlinecode">nodemcuv2</code> | ||||
|               for ESP8266 NodeMCU boards. Note: Use <code class="inlinecode">esp01_1m</code> for Sonoff devices. | ||||
|             </p> | ||||
|             <div class="input-field col s12"> | ||||
|               <input id="board_type" class="validate" type="text" name="board" required> | ||||
|               <label for="board_type">Board Type</label> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="step-actions"> | ||||
|             <button class="waves-effect waves-dark btn indigo next-step">CONTINUE</button> | ||||
|           </div> | ||||
|         </div> | ||||
|       </li> | ||||
|       <li class="step"> | ||||
|         <div class="step-title waves-effect">WiFi And Over-The-Air Updates</div> | ||||
|         <div class="step-content"> | ||||
|           <div class="row"> | ||||
|             <p> | ||||
|               Thanks! Now I need to know what WiFi Access Point I should instruct the node to connect to. | ||||
|               Please enter an SSID (name of the WiFi network) and password (leave empty for no password). | ||||
|             </p> | ||||
|             <div class="input-field col s12"> | ||||
|               <input id="wifi_ssid" class="validate" type="text" name="ssid" required> | ||||
|               <label for="wifi_ssid">WiFi SSID</label> | ||||
|             </div> | ||||
|             <div class="input-field col s12"> | ||||
|               <input id="wifi_password" name="psk" type="password"> | ||||
|               <label for="wifi_password">WiFi Password</label> | ||||
|             </div> | ||||
|             <p> | ||||
|               Esphomelib automatically sets up an Over-The-Air update server on the node | ||||
|               so that you only need to flash a firmware once. Optionally, you can set a password for this | ||||
|               upload process here. | ||||
|             </p> | ||||
|             <div class="input-field col s12"> | ||||
|               <input id="ota_password" class="validate" name="ota_password" type="password"> | ||||
|               <label for="ota_password">OTA Password</label> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="step-actions"> | ||||
|             <button class="waves-effect waves-dark btn indigo next-step">CONTINUE</button> | ||||
|           </div> | ||||
|         </div> | ||||
|       </li> | ||||
|       <li class="step"> | ||||
|         <div class="step-title waves-effect">MQTT</div> | ||||
|         <div class="step-content"> | ||||
|           <div class="row"> | ||||
|             <p> | ||||
|               esphomelib connects to your Home Assistant instance via | ||||
|               <a href="https://www.home-assistant.io/docs/mqtt/">MQTT</a>. If you haven't already, please set up | ||||
|               MQTT on your Home Assistant server, for example with the awesome | ||||
|               <a href="https://www.home-assistant.io/addons/mosquitto/">Mosquitto Hass.io Add-on</a>. | ||||
|             </p> | ||||
|             <p> | ||||
|               When you're done with that, please enter your MQTT broker here. For example | ||||
|               <code class="inlinecode">192.168.1.100</code> (Note | ||||
|               <code class="inlinecode">hassio.local</code> often doesn't work, please use a static IP). | ||||
|               Please also specify the MQTT username and password you wish esphomelib to use | ||||
|               (leave them empty if you're not using any authentication). | ||||
|             </p> | ||||
|             <div class="input-field col s12"> | ||||
|               <input id="mqtt_broker" class="validate" type="text" name="broker" required> | ||||
|               <label for="mqtt_broker">MQTT Broker</label> | ||||
|             </div> | ||||
|             <div class="input-field col s6"> | ||||
|               <input id="mqtt_username" class="validate" type="text" name="mqtt_username"> | ||||
|               <label for="mqtt_username">MQTT Username</label> | ||||
|             </div> | ||||
|             <div class="input-field col s6"> | ||||
|               <input id="mqtt_password" class="validate" name="mqtt_password" type="password"> | ||||
|               <label for="mqtt_password">MQTT Password</label> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="step-actions"> | ||||
|             <button class="waves-effect waves-dark btn indigo next-step">CONTINUE</button> | ||||
|           </div> | ||||
|         </div> | ||||
|       </li> | ||||
|       <li class="step"> | ||||
|         <div class="step-title waves-effect">Done!</div> | ||||
|         <div class="step-content"> | ||||
|           <p> | ||||
|             Hooray! 🎉🎉🎉 You've successfully created your first esphomeyaml configuration file. | ||||
|             When you click Submit, I will save this configuration file under | ||||
|             <code class="inlinecode"><HASS_CONFIG_FOLDER>/esphomeyaml/<NAME_OF_NODE>.yaml</code> and | ||||
|             you will be able to edit this file with the | ||||
|             <a href="https://www.home-assistant.io/addons/configurator/" target="_blank">HASS Configuratior add-on</a>. | ||||
|           </p> | ||||
|           <h5>Next steps</h5> | ||||
|           <ul class="browser-default"> | ||||
|             <li> | ||||
|               Flash the firmware. This can be done using the “UPLOAD” option in the dashboard. See | ||||
|               <a href="https://esphomelib.com/esphomeyaml/index.html#using-with" target="_blank">this</a> | ||||
|               for guides on how to flash different types of devices. Note that you need to restart this add-on | ||||
|               for newly plugged in serial devices to be detected. | ||||
|             </li> | ||||
|             <li> | ||||
|               See the <a href="https://esphomelib.com/esphomeyaml/index.html" target="_blank">esphomeyaml index</a> | ||||
|               for a list of supported sensors/devices. | ||||
|             </li> | ||||
|             <li> | ||||
|               Join the <a href="https://discord.gg/KhAMKrd" target="_blank">Discord server</a> and say hi. When I | ||||
|               have time, I would be happy to help with issues and discuss new features. | ||||
|             </li> | ||||
|             <li> | ||||
|               Star <a href="https://github.com/OttoWinter/esphomelib" target="_blank">esphomelib</a> and | ||||
|               <a href="https://github.com/OttoWinter/esphomeyaml" target="_blank">esphomeyaml</a> on GitHub and | ||||
|               report issues using the bug trackers there. | ||||
|             </li> | ||||
|           </ul> | ||||
|           <div class="step-actions"> | ||||
|             <button class="waves-effect waves-dark btn indigo" type="submit">SUBMIT</button> | ||||
|           </div> | ||||
|         </div> | ||||
|       </li> | ||||
|     </ul> | ||||
|     </form> | ||||
|   </div> | ||||
|   <div class="modal-footer"> | ||||
|     <a href="#!" class="modal-close waves-effect waves-green btn-flat">Abort</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> | ||||
|  | ||||
| <div class="tap-target pink lighten-1" data-target="setup-wizard-start"> | ||||
|   <div class="tap-target-content"> | ||||
|     <h5>Set up your first Node</h5> | ||||
|     <p> | ||||
|       Huh... It seems like you you don't have any esphomeyaml configuration files yet... | ||||
|       Fortunately, there's a setup wizard that will step you through setting up your first node 🎉 | ||||
|     </p> | ||||
|   </div> | ||||
| </div> | ||||
| </main> | ||||
|  | ||||
| <footer class="page-footer indigo darken-1"> | ||||
|   <div class="container"> | ||||
|  | ||||
|   </div> | ||||
|   <div class="footer-copyright"> | ||||
|     <div class="container"> | ||||
|       © 2018 Copyright Otto Winter, Made with <a class="grey-text text-lighten-4" href="https://materializecss.com/" target="_blank">Materialize</a> | ||||
|       <a class="grey-text text-lighten-4 right" href="https://esphomelib.com/esphomeyaml/index.html" target="_blank">esphomeyaml {{ version }} Documentation</a> | ||||
|     </div> | ||||
|   </div> | ||||
| </footer> | ||||
|  | ||||
| <script> | ||||
|   document.addEventListener('DOMContentLoaded', () => { | ||||
|     M.AutoInit(document.body); | ||||
|   }); | ||||
|  | ||||
|   const colorReplace = (input) => { | ||||
|     input = input.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"); | ||||
|     input = input.replace(/\\033\[(?:0;)?31m/g, '<span class="e">'); | ||||
|     input = input.replace(/\\033\[(?:1;)?31m/g, '<span class="e bold">'); | ||||
|     input = input.replace(/\\033\[(?:0;)?32m/g, '<span class="i">'); | ||||
|     input = input.replace(/\\033\[(?:1;)?32m/g, '<span class="i bold">'); | ||||
|     input = input.replace(/\\033\[(?:0;)?33m/g, '<span class="w">'); | ||||
|     input = input.replace(/\\033\[(?:1;)?33m/g, '<span class="w bold">'); | ||||
|     input = input.replace(/\\033\[(?:0;)?35m/g, '<span class="c">'); | ||||
|     input = input.replace(/\\033\[(?:1;)?35m/g, '<span class="c bold">'); | ||||
|     input = input.replace(/\\033\[(?:0;)?36m/g, '<span class="d">'); | ||||
|     input = input.replace(/\\033\[(?:1;)?36m/g, '<span class="d bold">'); | ||||
|     input = input.replace(/\\033\[(?:0;)?37m/g, '<span class="v">'); | ||||
|     input = input.replace(/\\033\[(?:1;)?37m/g, '<span class="v bold">'); | ||||
|     input = input.replace(/\\033\[(?:0;)?38m/g, '<span class="vv">'); | ||||
|     input = input.replace(/\\033\[(?:1;)?38m/g, '<span class="vv bold">'); | ||||
|     input = input.replace(/\\033\[0m/g, '</span>'); | ||||
|  | ||||
|     return input; | ||||
|   }; | ||||
|  | ||||
|   let configuration = ""; | ||||
|   const ws_url = 'ws://' + window.location.hostname + ':' + window.location.port; | ||||
|  | ||||
|   const logsModalElem = document.getElementById("modal-logs"); | ||||
|   const logsPortSelect = logsModalElem.querySelector('select'); | ||||
|   const logsPortDiv = logsModalElem.querySelector(".upload-port"); | ||||
|   const logsPortSubmit = logsModalElem.querySelector('.upload-port-submit'); | ||||
|   let logsStart = undefined; | ||||
|  | ||||
|   logsPortSubmit.addEventListener('click', () => { | ||||
|     const inst = M.FormSelect.getInstance(logsPortSelect); | ||||
|     logsStart(inst.getSelectedValues()[0]); | ||||
|     inst.destroy(); | ||||
|   }); | ||||
|  | ||||
|   document.querySelectorAll(".action-show-logs").forEach((showLogs) => { | ||||
|     showLogs.addEventListener('click', (e) => { | ||||
|       configuration = e.target.getAttribute('data-node'); | ||||
|       const modalInstance = M.Modal.getInstance(logsModalElem); | ||||
|       const log = logsModalElem.querySelector(".log"); | ||||
|       log.innerHTML = ""; | ||||
|  | ||||
|       if (M.FormSelect.getInstance(logsPortSelect) !== undefined) { | ||||
|         M.FormSelect.getInstance(logsPortSelect).destroy(); | ||||
|       } | ||||
|       modalInstance.open(); | ||||
|  | ||||
|       if (logsPortDiv.classList.contains('hide')) { | ||||
|         logsPortDiv.classList.remove('hide'); | ||||
|       } | ||||
|  | ||||
|       logsStart = (port) => { | ||||
|         logsPortDiv.classList.add('hide'); | ||||
|         const logSocket = new WebSocket(ws_url + "/logs"); | ||||
|         logSocket.addEventListener('message', (event) => { | ||||
|           const data = JSON.parse(event.data); | ||||
|           if (data.event === "line") { | ||||
|             const msg = data.data; | ||||
|             log.innerHTML += colorReplace(msg); | ||||
|           } else if (data.event === "exit") { | ||||
|             if (data.code === 0) { | ||||
|               M.toast({html: "Program exited successfully!"}); | ||||
|             } else { | ||||
|               M.toast({html: `Program failed with code ${data.code}`}); | ||||
|             } | ||||
|           } | ||||
|         }); | ||||
|         logSocket.addEventListener('open', () => { | ||||
|           const msg = JSON.stringify({configuration: configuration, port: port}); | ||||
|           logSocket.send(msg); | ||||
|         }); | ||||
|         logSocket.addEventListener('close', () => { | ||||
|           M.toast({html: 'Terminated process.'}); | ||||
|         }); | ||||
|         modalInstance.options.onCloseStart = () => { | ||||
|           logSocket.close(); | ||||
|         }; | ||||
|       }; | ||||
|  | ||||
|       fetch('/serial-ports').then(res => res.json()) | ||||
|         .then(response => { | ||||
|           if (response.length > 1) { | ||||
|             logsPortSelect.innerHTML = ""; | ||||
|             for (let i = 0; i < response.length; i++) { | ||||
|               const val = response[i]; | ||||
|               logsPortSelect.innerHTML += `<option value="${val.port}">${val.port} (${val.desc})</option>`; | ||||
|             } | ||||
|             M.FormSelect.init(logsPortSelect, {}); | ||||
|           } else { | ||||
|             logsStart("OTA"); | ||||
|           } | ||||
|         }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   const uploadModalElem = document.getElementById("modal-upload"); | ||||
|   const uploadPortSelect = uploadModalElem.querySelector('select'); | ||||
|   const uploadPortDiv = uploadModalElem.querySelector(".upload-port"); | ||||
|   const uploadPortSubmit = uploadModalElem.querySelector('.upload-port-submit'); | ||||
|   let uploadStart = undefined; | ||||
|  | ||||
|   uploadPortSubmit.addEventListener('click', () => { | ||||
|     const inst = M.FormSelect.getInstance(uploadPortSelect); | ||||
|     uploadStart(inst.getSelectedValues()[0]); | ||||
|     inst.destroy(); | ||||
|   }); | ||||
|  | ||||
|   document.querySelectorAll(".action-upload").forEach((showLogs) => { | ||||
|     showLogs.addEventListener('click', (e) => { | ||||
|       configuration = e.target.getAttribute('data-node'); | ||||
|       const modalInstance = M.Modal.getInstance(uploadModalElem); | ||||
|       const log = uploadModalElem.querySelector(".log"); | ||||
|       log.innerHTML = ""; | ||||
|  | ||||
|       if (M.FormSelect.getInstance(uploadPortSelect) !== undefined) { | ||||
|         M.FormSelect.getInstance(uploadPortSelect).destroy(); | ||||
|       } | ||||
|       modalInstance.open(); | ||||
|  | ||||
|       if (uploadPortDiv.classList.contains('hide')) { | ||||
|         uploadPortDiv.classList.remove('hide'); | ||||
|       } | ||||
|  | ||||
|       uploadStart = (port) => { | ||||
|         uploadPortDiv.classList.add('hide'); | ||||
|         const logSocket = new WebSocket(ws_url + "/run"); | ||||
|         logSocket.addEventListener('message', (event) => { | ||||
|           const data = JSON.parse(event.data); | ||||
|           if (data.event === "line") { | ||||
|             const msg = data.data; | ||||
|             log.innerHTML += colorReplace(msg); | ||||
|           } else if (data.event === "exit") { | ||||
|             if (data.code === 0) { | ||||
|               M.toast({html: "Program exited successfully!"}); | ||||
|             } else { | ||||
|               M.toast({html: `Program failed with code ${data.code}`}); | ||||
|             } | ||||
|           } | ||||
|         }); | ||||
|         logSocket.addEventListener('open', () => { | ||||
|           const msg = JSON.stringify({configuration: configuration, port: port}); | ||||
|           logSocket.send(msg); | ||||
|         }); | ||||
|         logSocket.addEventListener('close', () => { | ||||
|           M.toast({html: 'Terminated process.'}); | ||||
|         }); | ||||
|         modalInstance.options.onCloseStart = () => { | ||||
|           logSocket.close(); | ||||
|         }; | ||||
|       }; | ||||
|  | ||||
|       fetch('/serial-ports').then(res => res.json()) | ||||
|         .then(response => { | ||||
|           if (response.length > 1) { | ||||
|             uploadPortSelect.innerHTML = ""; | ||||
|             for (let i = 0; i < response.length; i++) { | ||||
|               const val = response[i]; | ||||
|               uploadPortSelect.innerHTML += `<option value="${val.port}">${val.port} (${val.desc})</option>`; | ||||
|             } | ||||
|             M.FormSelect.init(uploadPortSelect, {}); | ||||
|           } else { | ||||
|             uploadStart("OTA"); | ||||
|           } | ||||
|         }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   const compileModalElem = document.getElementById("modal-compile"); | ||||
|   const downloadButton = compileModalElem.querySelector('.download-binary'); | ||||
|  | ||||
|   document.querySelectorAll(".action-compile").forEach((showLogs) => { | ||||
|     showLogs.addEventListener('click', (e) => { | ||||
|       configuration = e.target.getAttribute('data-node'); | ||||
|       const modalInstance = M.Modal.getInstance(compileModalElem); | ||||
|       const log = compileModalElem.querySelector(".log"); | ||||
|       log.innerHTML = ""; | ||||
|       downloadButton.classList.add('disabled'); | ||||
|       modalInstance.open(); | ||||
|  | ||||
|       const logSocket = new WebSocket(ws_url + "/compile"); | ||||
|       logSocket.addEventListener('message', (event) => { | ||||
|         const data = JSON.parse(event.data); | ||||
|         if (data.event === "line") { | ||||
|           const msg = data.data; | ||||
|           log.innerHTML += colorReplace(msg); | ||||
|         } else if (data.event === "exit") { | ||||
|           if (data.code === 0) { | ||||
|             M.toast({html: "Program exited successfully!"}); | ||||
|             downloadButton.classList.remove('disabled'); | ||||
|           } else { | ||||
|             M.toast({html: `Program failed with code ${data.code}`}); | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|       logSocket.addEventListener('open', () => { | ||||
|         const msg = JSON.stringify({configuration: configuration}); | ||||
|         logSocket.send(msg); | ||||
|       }); | ||||
|       logSocket.addEventListener('close', () => { | ||||
|         M.toast({html: 'Terminated process.'}); | ||||
|       }); | ||||
|       modalInstance.options.onCloseStart = () => { | ||||
|         logSocket.close(); | ||||
|       }; | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   downloadButton.addEventListener('click', () => { | ||||
|     const link = document.createElement("a"); | ||||
|     link.download = name; | ||||
|     link.href = '/download.bin?configuration=' + encodeURIComponent(configuration); | ||||
|     link.click(); | ||||
|   }); | ||||
|  | ||||
|   const modalSetupElem = document.getElementById("modal-wizard"); | ||||
|   const setupWizardStart = document.getElementById('setup-wizard-start'); | ||||
|   const startWizard = () => { | ||||
|     const modalInstance = M.Modal.getInstance(modalSetupElem); | ||||
|     modalInstance.open(); | ||||
|  | ||||
|     modalInstance.options.onCloseStart = () => { | ||||
|  | ||||
|     }; | ||||
|  | ||||
|     $('.stepper').activateStepper({ | ||||
|       linearStepsNavigation: false, | ||||
|       autoFocusInput: true, | ||||
|       autoFormCreation: true, | ||||
|       showFeedbackLoader: true, | ||||
|       parallel: false | ||||
|     }); | ||||
|   }; | ||||
|   setupWizardStart.addEventListener('click', startWizard); | ||||
| </script> | ||||
|  | ||||
| {% if len(files) == 0 %} | ||||
| <script> | ||||
|   document.addEventListener('DOMContentLoaded', () => { | ||||
|     const tapTargetElem = document.querySelector('.tap-target'); | ||||
|     const tapTargetInstance = M.TapTarget.getInstance(tapTargetElem); | ||||
|     tapTargetInstance.options.onOpen = () => { | ||||
|       $('.tap-target-origin').on('click', () => { | ||||
|         startWizard(); | ||||
|       }); | ||||
|     }; | ||||
|     tapTargetInstance.open(); | ||||
|   }); | ||||
| </script> | ||||
| {% end %} | ||||
|  | ||||
| </body> | ||||
| </html> | ||||
		Reference in New Issue
	
	Block a user