mirror of
https://github.com/Threnklyn/esphome-dev.git
synced 2026-06-01 18:48:28 +02:00
Changes for 1.12 (#482)
* Update setup scripts * Add delete action, remove Hass config command * Update esphome.js * Lint
This commit is contained in:
@@ -3,11 +3,13 @@ from __future__ import print_function
|
||||
|
||||
import codecs
|
||||
import collections
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
@@ -28,7 +30,7 @@ from esphome.__main__ import get_serial_ports
|
||||
from esphome.helpers import mkdir_p, get_bool_env, run_system_command
|
||||
from esphome.py_compat import IS_PY2
|
||||
from esphome.storage_json import EsphomeStorageJSON, StorageJSON, \
|
||||
esphome_storage_path, ext_storage_path
|
||||
esphome_storage_path, ext_storage_path, trash_storage_path
|
||||
from esphome.util import shlex_quote
|
||||
|
||||
# pylint: disable=unused-import, wrong-import-order
|
||||
@@ -64,6 +66,27 @@ def template_args():
|
||||
}
|
||||
|
||||
|
||||
def authenticated(func):
|
||||
def decorator(self, *args, **kwargs):
|
||||
if not self.is_authenticated():
|
||||
self.redirect(RELATIVE_URL + 'login')
|
||||
return None
|
||||
return func(self, *args, **kwargs)
|
||||
return decorator
|
||||
|
||||
|
||||
def bind_config(func):
|
||||
def decorator(self, *args, **kwargs):
|
||||
configuration = self.get_argument('configuration')
|
||||
if not is_allowed(configuration):
|
||||
self.set_status(500)
|
||||
return None
|
||||
kwargs = kwargs.copy()
|
||||
kwargs['configuration'] = configuration
|
||||
return func(self, *args, **kwargs)
|
||||
return decorator
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class BaseHandler(tornado.web.RequestHandler):
|
||||
def is_authenticated(self):
|
||||
@@ -178,10 +201,8 @@ class EsphomeHassConfigHandler(EsphomeCommandWebSocket):
|
||||
|
||||
|
||||
class SerialPortRequestHandler(BaseHandler):
|
||||
@authenticated
|
||||
def get(self):
|
||||
if not self.is_authenticated():
|
||||
self.redirect(RELATIVE_URL + 'login')
|
||||
return
|
||||
ports = get_serial_ports()
|
||||
data = []
|
||||
for port, desc in ports:
|
||||
@@ -198,12 +219,10 @@ class SerialPortRequestHandler(BaseHandler):
|
||||
|
||||
|
||||
class WizardRequestHandler(BaseHandler):
|
||||
@authenticated
|
||||
def post(self):
|
||||
from esphome import wizard
|
||||
|
||||
if not self.is_authenticated():
|
||||
self.redirect(RELATIVE_URL + 'login')
|
||||
return
|
||||
kwargs = {k: ''.join(v) for k, v in self.request.arguments.items()}
|
||||
destination = os.path.join(CONFIG_DIR, kwargs['name'] + '.yaml')
|
||||
wizard.wizard_write(path=destination, **kwargs)
|
||||
@@ -211,13 +230,10 @@ class WizardRequestHandler(BaseHandler):
|
||||
|
||||
|
||||
class DownloadBinaryRequestHandler(BaseHandler):
|
||||
def get(self):
|
||||
if not self.is_authenticated():
|
||||
self.redirect(RELATIVE_URL + 'login')
|
||||
return
|
||||
|
||||
@authenticated
|
||||
@bind_config
|
||||
def get(self, configuration=None):
|
||||
# pylint: disable=no-value-for-parameter
|
||||
configuration = self.get_argument('configuration')
|
||||
storage_path = ext_storage_path(CONFIG_DIR, configuration)
|
||||
storage_json = StorageJSON.load(storage_path)
|
||||
if storage_json is None:
|
||||
@@ -315,11 +331,8 @@ class DashboardEntry(object):
|
||||
|
||||
|
||||
class MainRequestHandler(BaseHandler):
|
||||
@authenticated
|
||||
def get(self):
|
||||
if not self.is_authenticated():
|
||||
self.redirect(RELATIVE_URL + 'login')
|
||||
return
|
||||
|
||||
begin = bool(self.get_argument('begin', False))
|
||||
entries = _list_dashboard_entries()
|
||||
|
||||
@@ -400,11 +413,8 @@ class PingStatusThread(threading.Thread):
|
||||
|
||||
|
||||
class PingRequestHandler(BaseHandler):
|
||||
@authenticated
|
||||
def get(self):
|
||||
if not self.is_authenticated():
|
||||
self.redirect(RELATIVE_URL + 'login')
|
||||
return
|
||||
|
||||
PING_REQUEST.set()
|
||||
self.write(json.dumps(PING_RESULT))
|
||||
|
||||
@@ -414,34 +424,52 @@ def is_allowed(configuration):
|
||||
|
||||
|
||||
class EditRequestHandler(BaseHandler):
|
||||
def get(self):
|
||||
if not self.is_authenticated():
|
||||
self.redirect(RELATIVE_URL + 'login')
|
||||
return
|
||||
@authenticated
|
||||
@bind_config
|
||||
def get(self, configuration=None):
|
||||
# pylint: disable=no-value-for-parameter
|
||||
configuration = self.get_argument('configuration')
|
||||
if not is_allowed(configuration):
|
||||
self.set_status(401)
|
||||
return
|
||||
|
||||
with open(os.path.join(CONFIG_DIR, configuration), 'r') as f:
|
||||
content = f.read()
|
||||
self.write(content)
|
||||
|
||||
def post(self):
|
||||
if not self.is_authenticated():
|
||||
self.redirect(RELATIVE_URL + 'login')
|
||||
return
|
||||
@authenticated
|
||||
@bind_config
|
||||
def post(self, configuration=None):
|
||||
# pylint: disable=no-value-for-parameter
|
||||
configuration = self.get_argument('configuration')
|
||||
if not is_allowed(configuration):
|
||||
self.set_status(401)
|
||||
return
|
||||
|
||||
with open(os.path.join(CONFIG_DIR, configuration), 'wb') as f:
|
||||
f.write(self.request.body)
|
||||
self.set_status(200)
|
||||
return
|
||||
|
||||
|
||||
class DeleteRequestHandler(BaseHandler):
|
||||
@authenticated
|
||||
@bind_config
|
||||
def post(self, configuration=None):
|
||||
config_file = os.path.join(CONFIG_DIR, configuration)
|
||||
storage_path = ext_storage_path(CONFIG_DIR, configuration)
|
||||
storage_json = StorageJSON.load(storage_path)
|
||||
if storage_json is None:
|
||||
self.set_status(500)
|
||||
return
|
||||
|
||||
name = storage_json.name
|
||||
trash_path = trash_storage_path(CONFIG_DIR)
|
||||
mkdir_p(trash_path)
|
||||
shutil.move(config_file, os.path.join(trash_path, configuration))
|
||||
|
||||
# Delete build folder (if exists)
|
||||
build_folder = os.path.join(CONFIG_DIR, name)
|
||||
if build_folder is not None:
|
||||
shutil.rmtree(build_folder, os.path.join(trash_path, name))
|
||||
|
||||
|
||||
class UndoDeleteRequestHandler(BaseHandler):
|
||||
@authenticated
|
||||
@bind_config
|
||||
def post(self, configuration=None):
|
||||
config_file = os.path.join(CONFIG_DIR, configuration)
|
||||
trash_path = trash_storage_path(CONFIG_DIR)
|
||||
shutil.move(os.path.join(trash_path, configuration), config_file)
|
||||
|
||||
|
||||
PING_RESULT = {} # type: dict
|
||||
@@ -511,9 +539,9 @@ def get_static_file_url(name):
|
||||
else:
|
||||
path = os.path.join(static_path, name)
|
||||
with open(path, 'rb') as f_handle:
|
||||
hash_ = hash(f_handle.read()) & (2**32-1)
|
||||
hash_ = hashlib.md5(f_handle.read()).hexdigest()[:8]
|
||||
_STATIC_FILE_HASHES[name] = hash_
|
||||
return RELATIVE_URL + u'static/{}?hash={:08X}'.format(name, hash_)
|
||||
return RELATIVE_URL + u'static/{}?hash={}'.format(name, hash_)
|
||||
|
||||
|
||||
def make_app(debug=False):
|
||||
@@ -561,6 +589,8 @@ def make_app(debug=False):
|
||||
(RELATIVE_URL + "download.bin", DownloadBinaryRequestHandler),
|
||||
(RELATIVE_URL + "serial-ports", SerialPortRequestHandler),
|
||||
(RELATIVE_URL + "ping", PingRequestHandler),
|
||||
(RELATIVE_URL + "delete", DeleteRequestHandler),
|
||||
(RELATIVE_URL + "undo-delete", UndoDeleteRequestHandler),
|
||||
(RELATIVE_URL + "wizard.html", WizardRequestHandler),
|
||||
(RELATIVE_URL + r"static/(.*)", StaticFileHandler, {'path': static_path}),
|
||||
], **settings)
|
||||
|
||||
@@ -590,51 +590,30 @@ document.querySelectorAll(".action-clean").forEach((btn) => {
|
||||
});
|
||||
});
|
||||
|
||||
const hassConfigModalElem = document.getElementById("modal-hass-config");
|
||||
|
||||
document.querySelectorAll(".action-hass-config").forEach((btn) => {
|
||||
document.querySelectorAll(".action-delete").forEach((btn) => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
configuration = e.target.getAttribute('data-node');
|
||||
const modalInstance = M.Modal.getInstance(hassConfigModalElem);
|
||||
const log = hassConfigModalElem.querySelector(".log");
|
||||
log.innerHTML = "";
|
||||
const colorState = initializeColorState();
|
||||
const stopLogsButton = hassConfigModalElem.querySelector(".stop-logs");
|
||||
let stopped = false;
|
||||
stopLogsButton.innerHTML = "Stop";
|
||||
modalInstance.open();
|
||||
|
||||
const filenameField = hassConfigModalElem.querySelector('.filename');
|
||||
filenameField.innerHTML = configuration;
|
||||
fetch(`${relative_url}delete?configuration=${configuration}`, {
|
||||
credentials: "same-origin",
|
||||
method: "POST",
|
||||
}).then(res => res.text()).then(() => {
|
||||
const toastHtml = `<span>Deleted <code class="inlinecode">${configuration}</code>
|
||||
<button class="btn-flat toast-action">Undo</button></button>`;
|
||||
const toast = M.toast({html: toastHtml});
|
||||
const undoButton = toast.el.querySelector('.toast-action');
|
||||
|
||||
const logSocket = new WebSocket(wsUrl + "hass-config");
|
||||
logSocket.addEventListener('message', (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.event === "line") {
|
||||
colorReplace(log, colorState, data.data);
|
||||
} 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;
|
||||
}
|
||||
document.querySelector(`.entry-row[data-node="${configuration}"]`).remove();
|
||||
|
||||
undoButton.addEventListener('click', () => {
|
||||
fetch(`${relative_url}undo-delete?configuration=${configuration}`, {
|
||||
credentials: "same-origin",
|
||||
method: "POST",
|
||||
}).then(res => res.text()).then(() => {
|
||||
window.location.reload(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
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();
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<main>
|
||||
<div class="container">
|
||||
{% for i, entry in enumerate(entries) %}
|
||||
<div class="row">
|
||||
<div class="row entry-row" data-node="{{ entry.filename }}">
|
||||
<div class="col s12 m10 offset-m1 l12">
|
||||
<div class="card horizontal">
|
||||
<div class="card-image center-align hide-on-small-only">
|
||||
@@ -93,7 +93,7 @@
|
||||
<li><a class="action-clean-mqtt" data-node="{{ entry.filename }}">Clean MQTT</a></li>
|
||||
<li><a class="action-clean" data-node="{{ entry.filename }}">Clean Build</a></li>
|
||||
<li><a class="action-compile" data-node="{{ entry.filename }}">Compile</a></li>
|
||||
<li><a class="action-hass-config" data-node="{{ entry.filename }}">HASS MQTT Configuration</a></li>
|
||||
<li><a class="action-delete" data-node="{{ entry.filename }}">Delete</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -422,16 +422,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="modal-hass-config" class="modal modal-fixed-footer">
|
||||
<div class="modal-content">
|
||||
<h4>Generate Home Assistant Configuration <code class="inlinecode filename"></code></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>
|
||||
|
||||
<div id="modal-editor" class="modal modal-fixed-footer">
|
||||
<div class="modal-content">
|
||||
<h4>Edit <code class="inlinecode filename"></code></h4>
|
||||
|
||||
Reference in New Issue
Block a user