Fix dashboard upload port selection

This commit is contained in:
Otto Winter 2018-06-03 11:18:53 +02:00
parent 967aa53bad
commit f4d393a59e
No known key found for this signature in database
GPG key ID: DB66C0BE6013F97E
3 changed files with 235 additions and 199 deletions

View file

@ -102,11 +102,14 @@ class SerialPortRequestHandler(tornado.web.RequestHandler):
data = [] data = []
for port, desc in ports: for port, desc in ports:
if port == '/dev/ttyAMA0': if port == '/dev/ttyAMA0':
# ignore RPi built-in serial port desc = 'UART pins on GPIO header'
continue split_desc = desc.split(' - ')
if len(split_desc) == 2 and split_desc[0] == split_desc[1]:
# Some serial ports repeat their values
desc = split_desc[0]
data.append({'port': port, 'desc': desc}) data.append({'port': port, 'desc': desc})
data.append({'port': 'OTA', 'desc': 'Over-The-Air Upload/Logs'}) data.append({'port': 'OTA', 'desc': 'Over-The-Air'})
self.write(json.dumps(data)) self.write(json.dumps(sorted(data, reverse=True)))
class WizardRequestHandler(tornado.web.RequestHandler): class WizardRequestHandler(tornado.web.RequestHandler):
@ -119,7 +122,7 @@ class WizardRequestHandler(tornado.web.RequestHandler):
with codecs.open(destination, 'w') as f_handle: with codecs.open(destination, 'w') as f_handle:
f_handle.write(config) f_handle.write(config)
self.redirect('/') self.redirect('/?begin=True')
class DownloadBinaryRequestHandler(tornado.web.RequestHandler): class DownloadBinaryRequestHandler(tornado.web.RequestHandler):
@ -143,14 +146,15 @@ class DownloadBinaryRequestHandler(tornado.web.RequestHandler):
class MainRequestHandler(tornado.web.RequestHandler): class MainRequestHandler(tornado.web.RequestHandler):
def get(self): def get(self):
begin = bool(self.get_argument('begin', False))
files = sorted([f for f in os.listdir(CONFIG_DIR) if f.endswith('.yaml') and files = sorted([f for f in os.listdir(CONFIG_DIR) if f.endswith('.yaml') and
not f.startswith('.')]) not f.startswith('.')])
full_path_files = [os.path.join(CONFIG_DIR, f) for f in files] 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, self.render("templates/index.html", files=files, full_path_files=full_path_files,
version=const.__version__) version=const.__version__, begin=begin)
def make_app(): def make_app(debug=False):
static_path = os.path.join(os.path.dirname(__file__), 'static') static_path = os.path.join(os.path.dirname(__file__), 'static')
return tornado.web.Application([ return tornado.web.Application([
(r"/", MainRequestHandler), (r"/", MainRequestHandler),
@ -161,7 +165,7 @@ def make_app():
(r"/serial-ports", SerialPortRequestHandler), (r"/serial-ports", SerialPortRequestHandler),
(r"/wizard.html", WizardRequestHandler), (r"/wizard.html", WizardRequestHandler),
(r'/static/(.*)', tornado.web.StaticFileHandler, {'path': static_path}), (r'/static/(.*)', tornado.web.StaticFileHandler, {'path': static_path}),
], debug=False) ], debug=debug)
def start_web_server(args): def start_web_server(args):
@ -177,7 +181,7 @@ def start_web_server(args):
_LOGGER.info("Starting dashboard web server on port %s and configuration dir %s...", _LOGGER.info("Starting dashboard web server on port %s and configuration dir %s...",
args.port, CONFIG_DIR) args.port, CONFIG_DIR)
app = make_app() app = make_app(args.verbose)
app.listen(args.port) app.listen(args.port)
try: try:
tornado.ioloop.IOLoop.current().start() tornado.ioloop.IOLoop.current().start()

View file

@ -121,9 +121,9 @@
} }
.modal { .modal {
width: 90%; width: 95%;
max-height: 85%; max-height: 90%;
height: 80% !important; height: 85% !important;
} }
.page-footer { .page-footer {
@ -155,7 +155,9 @@
} }
.select-port-container { .select-port-container {
margin-top: 19px; margin-top: 8px;
margin-right: 24px;
width: 350px;
} }
</style> </style>
</head> </head>
@ -165,9 +167,23 @@
<nav> <nav>
<div class="nav-wrapper indigo"> <div class="nav-wrapper indigo">
<a href="#" class="brand-logo left">esphomeyaml Dashboard</a> <a href="#" class="brand-logo left">esphomeyaml Dashboard</a>
<div class="select-port-container right" id="select-port-target">
<select></select>
</div>
</div> </div>
</nav> </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> <div class="ribbon"></div>
</header> </header>
@ -201,64 +217,38 @@
<div id="modal-logs" class="modal modal-fixed-footer"> <div id="modal-logs" class="modal modal-fixed-footer">
<div class="modal-content"> <div class="modal-content">
<h4>Show Logs</h4> <h4>Show Logs <code class="inlinecode filename"></code></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"> <div class="log-container">
<pre class="log"></pre> <pre class="log"></pre>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<a class="modal-close waves-effect waves-green btn-flat">Close</a> <a class="modal-close waves-effect waves-green btn-flat stop-logs">Close</a>
</div> </div>
</div> </div>
<div id="modal-upload" class="modal modal-fixed-footer"> <div id="modal-upload" class="modal modal-fixed-footer">
<div class="modal-content"> <div class="modal-content">
<h4>Compile And Upload</h4> <h4>Compile And Upload <code class="inlinecode filename"></code></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"> <div class="log-container">
<pre class="log"></pre> <pre class="log"></pre>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<a class="modal-close waves-effect waves-green btn-flat">Stop</a> <a class="modal-close waves-effect waves-green btn-flat stop-logs">Stop</a>
</div> </div>
</div> </div>
<div id="modal-compile" class="modal modal-fixed-footer"> <div id="modal-compile" class="modal modal-fixed-footer">
<div class="modal-content"> <div class="modal-content">
<h4>Compile</h4> <h4>Compile <code class="inlinecode filename"></code></h4>
<div class="log-container"> <div class="log-container">
<pre class="log"></pre> <pre class="log"></pre>
</div> </div>
</div> </div>
<div class="modal-footer"> <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 disabled download-binary">Download Binary</a>
<a class="modal-close waves-effect waves-green btn-flat">Stop</a> <a class="modal-close waves-effect waves-green btn-flat stop-logs">Stop</a>
</div> </div>
</div> </div>
@ -303,7 +293,7 @@
</div> </div>
</div> </div>
<div class="step-actions"> <div class="step-actions">
<button class="waves-effect waves-dark btn indigo next-step"">CONTINUE</button> <button class="waves-effect waves-dark btn indigo next-step">CONTINUE</button>
</div> </div>
</div> </div>
</li> </li>
@ -455,7 +445,7 @@
<i class="material-icons">add</i> <i class="material-icons">add</i>
</a> </a>
<div class="tap-target pink lighten-1" data-target="setup-wizard-start"> <div class="tap-target pink lighten-1 setup-wizard" data-target="setup-wizard-start">
<div class="tap-target-content"> <div class="tap-target-content">
<h5>Set up your first Node</h5> <h5>Set up your first Node</h5>
<p> <p>
@ -505,19 +495,67 @@
}; };
let configuration = ""; let configuration = "";
const ws_url = 'ws://' + window.location.hostname + ':' + window.location.port; 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, 2500);
fetchSerialPorts(true);
const logsModalElem = document.getElementById("modal-logs"); 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) => { document.querySelectorAll(".action-show-logs").forEach((showLogs) => {
showLogs.addEventListener('click', (e) => { showLogs.addEventListener('click', (e) => {
@ -525,19 +563,15 @@
const modalInstance = M.Modal.getInstance(logsModalElem); const modalInstance = M.Modal.getInstance(logsModalElem);
const log = logsModalElem.querySelector(".log"); const log = logsModalElem.querySelector(".log");
log.innerHTML = ""; log.innerHTML = "";
const stopLogsButton = logsModalElem.querySelector(".stop-logs");
if (M.FormSelect.getInstance(logsPortSelect) !== undefined) { let stopped = false;
M.FormSelect.getInstance(logsPortSelect).destroy(); stopLogsButton.innerHTML = "Stop";
}
modalInstance.open(); modalInstance.open();
if (logsPortDiv.classList.contains('hide')) { const filenameField = logsModalElem.querySelector('.filename');
logsPortDiv.classList.remove('hide'); filenameField.innerHTML = configuration;
}
logsStart = (port) => { const logSocket = new WebSocket(wsUrl + "/logs");
logsPortDiv.classList.add('hide');
const logSocket = new WebSocket(ws_url + "/logs");
logSocket.addEventListener('message', (event) => { logSocket.addEventListener('message', (event) => {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
if (data.event === "line") { if (data.event === "line") {
@ -545,71 +579,47 @@
log.innerHTML += colorReplace(msg); log.innerHTML += colorReplace(msg);
} else if (data.event === "exit") { } else if (data.event === "exit") {
if (data.code === 0) { if (data.code === 0) {
M.toast({html: "Program exited successfully!"}); M.toast({html: "Program exited successfully."});
} else { } else {
M.toast({html: `Program failed with code ${data.code}`}); M.toast({html: `Program failed with code ${data.code}`});
} }
stopLogsButton.innerHTML = "Close";
stopped = true;
} }
}); });
logSocket.addEventListener('open', () => { logSocket.addEventListener('open', () => {
const msg = JSON.stringify({configuration: configuration, port: port}); const msg = JSON.stringify({configuration: configuration, port: getUploadPort()});
logSocket.send(msg); logSocket.send(msg);
}); });
logSocket.addEventListener('close', () => { logSocket.addEventListener('close', () => {
if (!stopped) {
M.toast({html: 'Terminated process.'}); M.toast({html: 'Terminated process.'});
}
}); });
modalInstance.options.onCloseStart = () => { modalInstance.options.onCloseStart = () => {
logSocket.close(); 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 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', () => { document.querySelectorAll(".action-upload").forEach((upload) => {
const inst = M.FormSelect.getInstance(uploadPortSelect); upload.addEventListener('click', (e) => {
uploadStart(inst.getSelectedValues()[0]);
inst.destroy();
});
document.querySelectorAll(".action-upload").forEach((showLogs) => {
showLogs.addEventListener('click', (e) => {
configuration = e.target.getAttribute('data-node'); configuration = e.target.getAttribute('data-node');
const modalInstance = M.Modal.getInstance(uploadModalElem); const modalInstance = M.Modal.getInstance(uploadModalElem);
const log = uploadModalElem.querySelector(".log"); const log = uploadModalElem.querySelector(".log");
log.innerHTML = ""; log.innerHTML = "";
const stopLogsButton = uploadModalElem.querySelector(".stop-logs");
if (M.FormSelect.getInstance(uploadPortSelect) !== undefined) { let stopped = false;
M.FormSelect.getInstance(uploadPortSelect).destroy(); stopLogsButton.innerHTML = "Stop";
}
modalInstance.open(); modalInstance.open();
if (uploadPortDiv.classList.contains('hide')) { const filenameField = uploadModalElem.querySelector('.filename');
uploadPortDiv.classList.remove('hide'); filenameField.innerHTML = configuration;
}
uploadStart = (port) => { const logSocket = new WebSocket(wsUrl + "/run");
uploadPortDiv.classList.add('hide');
const logSocket = new WebSocket(ws_url + "/run");
logSocket.addEventListener('message', (event) => { logSocket.addEventListener('message', (event) => {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
if (data.event === "line") { if (data.event === "line") {
@ -617,53 +627,50 @@
log.innerHTML += colorReplace(msg); log.innerHTML += colorReplace(msg);
} else if (data.event === "exit") { } else if (data.event === "exit") {
if (data.code === 0) { if (data.code === 0) {
M.toast({html: "Program exited successfully!"}); M.toast({html: "Program exited successfully."});
} else { } else {
M.toast({html: `Program failed with code ${data.code}`}); M.toast({html: `Program failed with code ${data.code}`});
} }
stopLogsButton.innerHTML = "Close";
stopped = true;
} }
}); });
logSocket.addEventListener('open', () => { logSocket.addEventListener('open', () => {
const msg = JSON.stringify({configuration: configuration, port: port}); const msg = JSON.stringify({configuration: configuration, port: getUploadPort()});
logSocket.send(msg); logSocket.send(msg);
}); });
logSocket.addEventListener('close', () => { logSocket.addEventListener('close', () => {
if (!stopped) {
M.toast({html: 'Terminated process.'}); M.toast({html: 'Terminated process.'});
}
}); });
modalInstance.options.onCloseStart = () => { modalInstance.options.onCloseStart = () => {
logSocket.close(); 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 compileModalElem = document.getElementById("modal-compile");
const downloadButton = compileModalElem.querySelector('.download-binary'); const downloadButton = compileModalElem.querySelector('.download-binary');
document.querySelectorAll(".action-compile").forEach((showLogs) => { document.querySelectorAll(".action-compile").forEach((upload) => {
showLogs.addEventListener('click', (e) => { upload.addEventListener('click', (e) => {
configuration = e.target.getAttribute('data-node'); configuration = e.target.getAttribute('data-node');
const modalInstance = M.Modal.getInstance(compileModalElem); const modalInstance = M.Modal.getInstance(compileModalElem);
const log = compileModalElem.querySelector(".log"); const log = compileModalElem.querySelector(".log");
log.innerHTML = ""; log.innerHTML = "";
const stopLogsButton = compileModalElem.querySelector(".stop-logs");
let stopped = false;
stopLogsButton.innerHTML = "Stop";
downloadButton.classList.add('disabled'); downloadButton.classList.add('disabled');
modalInstance.open(); modalInstance.open();
const logSocket = new WebSocket(ws_url + "/compile"); const filenameField = compileModalElem.querySelector('.filename');
filenameField.innerHTML = configuration;
const logSocket = new WebSocket(wsUrl + "/compile");
logSocket.addEventListener('message', (event) => { logSocket.addEventListener('message', (event) => {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
if (data.event === "line") { if (data.event === "line") {
@ -671,11 +678,14 @@
log.innerHTML += colorReplace(msg); log.innerHTML += colorReplace(msg);
} else if (data.event === "exit") { } else if (data.event === "exit") {
if (data.code === 0) { if (data.code === 0) {
M.toast({html: "Program exited successfully!"}); M.toast({html: "Program exited successfully."});
downloadButton.classList.remove('disabled'); downloadButton.classList.remove('disabled');
} else { } else {
M.toast({html: `Program failed with code ${data.code}`}); M.toast({html: `Program failed with code ${data.code}`});
} }
stopLogsButton.innerHTML = "Close";
stopped = true;
} }
}); });
logSocket.addEventListener('open', () => { logSocket.addEventListener('open', () => {
@ -683,14 +693,15 @@
logSocket.send(msg); logSocket.send(msg);
}); });
logSocket.addEventListener('close', () => { logSocket.addEventListener('close', () => {
if (!stopped) {
M.toast({html: 'Terminated process.'}); M.toast({html: 'Terminated process.'});
}
}); });
modalInstance.options.onCloseStart = () => { modalInstance.options.onCloseStart = () => {
logSocket.close(); logSocket.close();
}; };
}); });
}); });
downloadButton.addEventListener('click', () => { downloadButton.addEventListener('click', () => {
const link = document.createElement("a"); const link = document.createElement("a");
link.download = name; link.download = name;
@ -722,7 +733,7 @@
{% if len(files) == 0 %} {% if len(files) == 0 %}
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const tapTargetElem = document.querySelector('.tap-target'); const tapTargetElem = document.querySelector('.tap-target.setup-wizard');
const tapTargetInstance = M.TapTarget.getInstance(tapTargetElem); const tapTargetInstance = M.TapTarget.getInstance(tapTargetElem);
tapTargetInstance.options.onOpen = () => { tapTargetInstance.options.onOpen = () => {
$('.tap-target-origin').on('click', () => { $('.tap-target-origin').on('click', () => {
@ -734,5 +745,23 @@
</script> </script>
{% end %} {% 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> </body>
</html> </html>

View file

@ -450,10 +450,12 @@ def flush_tasks():
raise ESPHomeYAMLError("Circular dependency detected!") raise ESPHomeYAMLError("Circular dependency detected!")
task, domain = _TASKS.popleft() task, domain = _TASKS.popleft()
_LOGGER.debug("Executing task for domain=%s", domain)
try: try:
task.next() task.next()
_TASKS.append((task, domain)) _TASKS.append((task, domain))
except StopIteration: except StopIteration:
_LOGGER.debug(" -> %s finished", domain)
pass pass
@ -461,6 +463,7 @@ def add(expression, require=True):
if require and isinstance(expression, Expression): if require and isinstance(expression, Expression):
expression.require() expression.require()
_EXPRESSIONS.append(expression) _EXPRESSIONS.append(expression)
_LOGGER.debug("Adding: %s", statement(expression))
return expression return expression