<!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; white-space: pre-wrap; overflow-wrap: break-word; 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: 95%; max-height: 90%; height: 85% !important; } .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: 8px; margin-right: 24px; width: 350px; } </style> </head> <body> <header> <nav> <div class="nav-wrapper indigo"> <a href="#" class="brand-logo left">esphomeyaml Dashboard</a> <div class="select-port-container right" id="select-port-target"> <select></select> </div> </div> </nav> <div class="tap-target pink lighten-1 select-port" data-target="select-port-target"> <div class="tap-target-content"> <h5>Select Upload Port</h5> <p> Here you can select where esphomeyaml will attempt to show logs and upload firmwares to. By default, this is "OTA", or Over-The-Air. Note that you might have to restart the HassIO add-on for new serial ports to be detected. </p> </div> </div> <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> <a href="#" class="action-validate" data-node="{{ file }}">Validate</a> </div> </div> </div> </div> </div> {% end %} </div> <div id="modal-logs" class="modal modal-fixed-footer"> <div class="modal-content"> <h4>Show Logs <code class="inlinecode filename"></code></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 stop-logs">Close</a> </div> </div> <div id="modal-upload" class="modal modal-fixed-footer"> <div class="modal-content"> <h4>Compile And Upload <code class="inlinecode filename"></code></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 stop-logs">Stop</a> </div> </div> <div id="modal-compile" class="modal modal-fixed-footer"> <div class="modal-content"> <h4>Compile <code class="inlinecode filename"></code></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-logs">Stop</a> </div> </div> <div id="modal-validate" class="modal modal-fixed-footer"> <div class="modal-content"> <h4>Validate <code class="inlinecode filename"></code></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 stop-logs">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="https://esphomelib.com/esphomeyaml/devices/nodemcu_esp8266.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). </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 via USB 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> doesn't always 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#devices" 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> With the current configuration, your node will only connect to WiFi and MQTT. To make it actually <i>do</i> stuff, follow <a href="https://esphomelib.com/esphomeyaml/guides/getting_started_hassio.html#adding-some-basic-features"> the rest of the getting started guide </a>. </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 if you find this software awesome 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 setup-wizard" 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 = ""; let wsProtocol = "ws:"; if (window.location.protocol === "https:") { wsProtocol = 'wss:'; } const wsUrl = wsProtocol + '//' + window.location.hostname + ':' + window.location.port; const portSelect = document.querySelector('.nav-wrapper select'); let ports = []; const fetchSerialPorts = (begin=false) => { fetch('/serial-ports').then(res => res.json()) .then(response => { if (ports.length === response.length) { let allEqual = true; for (let i = 0; i < response.length; i++) { if (ports[i].port !== response[i].port) { allEqual = false; break; } } if (allEqual) return; } ports = response; const inst = M.FormSelect.getInstance(portSelect); if (inst !== undefined) { inst.destroy(); } portSelect.innerHTML = ""; const prevSelected = getUploadPort(); for (let i = 0; i < response.length; i++) { const val = response[i]; if (val.port === prevSelected) { portSelect.innerHTML += `<option value="${val.port}" selected>${val.port} (${val.desc})</option>`; } else { portSelect.innerHTML += `<option value="${val.port}">${val.port} (${val.desc})</option>`; } } M.FormSelect.init(portSelect, {}); if (!begin) M.toast({html: "Discovered new serial port."}); }); }; const getUploadPort = () => { const inst = M.FormSelect.getInstance(portSelect); if (inst === undefined) { return "OTA"; } inst._setSelectedStates(); return inst.getSelectedValues()[0]; }; setInterval(fetchSerialPorts, 5000); fetchSerialPorts(true); const logsModalElem = document.getElementById("modal-logs"); 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 = ""; const stopLogsButton = logsModalElem.querySelector(".stop-logs"); let stopped = false; stopLogsButton.innerHTML = "Stop"; modalInstance.open(); const filenameField = logsModalElem.querySelector('.filename'); filenameField.innerHTML = configuration; const logSocket = new WebSocket(wsUrl + "/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}`}); } stopLogsButton.innerHTML = "Close"; stopped = true; } }); logSocket.addEventListener('open', () => { const msg = JSON.stringify({configuration: configuration, port: getUploadPort()}); logSocket.send(msg); }); logSocket.addEventListener('close', () => { if (!stopped) { M.toast({html: 'Terminated process.'}); } }); modalInstance.options.onCloseStart = () => { logSocket.close(); }; }); }); const uploadModalElem = document.getElementById("modal-upload"); document.querySelectorAll(".action-upload").forEach((upload) => { upload.addEventListener('click', (e) => { configuration = e.target.getAttribute('data-node'); const modalInstance = M.Modal.getInstance(uploadModalElem); const log = uploadModalElem.querySelector(".log"); log.innerHTML = ""; const stopLogsButton = uploadModalElem.querySelector(".stop-logs"); let stopped = false; stopLogsButton.innerHTML = "Stop"; modalInstance.open(); const filenameField = uploadModalElem.querySelector('.filename'); filenameField.innerHTML = configuration; const logSocket = new WebSocket(wsUrl + "/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}`}); } stopLogsButton.innerHTML = "Close"; stopped = true; } }); logSocket.addEventListener('open', () => { const msg = JSON.stringify({configuration: configuration, port: getUploadPort()}); logSocket.send(msg); }); logSocket.addEventListener('close', () => { if (!stopped) { M.toast({html: 'Terminated process.'}); } }); modalInstance.options.onCloseStart = () => { logSocket.close(); }; }); }); const validateModalElem = document.getElementById("modal-validate"); document.querySelectorAll(".action-validate").forEach((upload) => { upload.addEventListener('click', (e) => { configuration = e.target.getAttribute('data-node'); const modalInstance = M.Modal.getInstance(validateModalElem); const log = validateModalElem.querySelector(".log"); log.innerHTML = ""; const stopLogsButton = validateModalElem.querySelector(".stop-logs"); let stopped = false; stopLogsButton.innerHTML = "Stop"; modalInstance.open(); const filenameField = validateModalElem.querySelector('.filename'); filenameField.innerHTML = configuration; const logSocket = new WebSocket(wsUrl + "/validate"); 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: `<code class="inlinecode">${configuration}</code> is valid 👍`, displayLength: 5000, }); } else { M.toast({ html: `<code class="inlinecode">${configuration}</code> is invalid 😕`, displayLength: 5000, }); } stopLogsButton.innerHTML = "Close"; stopped = true; } }); logSocket.addEventListener('open', () => { const msg = JSON.stringify({configuration: configuration}); logSocket.send(msg); }); logSocket.addEventListener('close', () => { if (!stopped) { M.toast({html: 'Terminated process.'}); } }); modalInstance.options.onCloseStart = () => { logSocket.close(); }; }); }); const compileModalElem = document.getElementById("modal-compile"); const downloadButton = compileModalElem.querySelector('.download-binary'); document.querySelectorAll(".action-compile").forEach((upload) => { upload.addEventListener('click', (e) => { configuration = e.target.getAttribute('data-node'); const modalInstance = M.Modal.getInstance(compileModalElem); const log = compileModalElem.querySelector(".log"); log.innerHTML = ""; const stopLogsButton = compileModalElem.querySelector(".stop-logs"); let stopped = false; stopLogsButton.innerHTML = "Stop"; downloadButton.classList.add('disabled'); modalInstance.open(); const filenameField = compileModalElem.querySelector('.filename'); filenameField.innerHTML = configuration; const logSocket = new WebSocket(wsUrl + "/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}`}); } stopLogsButton.innerHTML = "Close"; stopped = true; } }); logSocket.addEventListener('open', () => { const msg = JSON.stringify({configuration: configuration}); logSocket.send(msg); }); logSocket.addEventListener('close', () => { if (!stopped) { 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.setup-wizard'); const tapTargetInstance = M.TapTarget.getInstance(tapTargetElem); tapTargetInstance.options.onOpen = () => { $('.tap-target-origin').on('click', () => { startWizard(); }); }; tapTargetInstance.open(); }); </script> {% end %} {% if begin %} <script> window.history.replaceState({}, document.title, "/"); document.addEventListener('DOMContentLoaded', () => { const tapTargetElem = document.querySelector('.tap-target.select-port'); const tapTargetInstance = M.TapTarget.getInstance(tapTargetElem); tapTargetInstance.open(); tapTargetInstance.contentEl.style["top"] = "300px"; tapTargetInstance.contentEl.style["padding"] = "250px"; tapTargetInstance.waveEl.style["top"] = "250px"; tapTargetInstance.waveEl.style["left"] = "250px"; tapTargetInstance.waveEl.style["width"] = "300px"; tapTargetInstance.waveEl.style["height"] = "300px"; }); </script> {% end %} </body> </html>