Add relative_url, streamer_mode, status_use_ping dashboard options (#461)

* Add relative_url, streamer_mode, status_use_ping dashboard options

Additionally Hass.io now stores all build files in /data, so that snapshots no longer get huge.

* Lint

* Lint

* Replace tabs with spaces
This commit is contained in:
Otto Winter 2019-03-03 16:50:06 +01:00 committed by GitHub
parent 5a102c2ab7
commit 067ec30c56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 178 additions and 71 deletions

View File

@ -6,9 +6,23 @@
# shellcheck disable=SC1091
source /usr/lib/hassio-addons/base.sh
export ESPHOME_IS_HASSIO=true
if hass.config.true 'leave_front_door_open'; then
export DISABLE_HA_AUTHENTICATION=true
fi
if hass.config.true 'streamer_mode'; then
export ESPHOME_STREAMER_MODE=true
fi
if hass.config.true 'status_use_ping'; then
export ESPHOME_DASHBOARD_USE_PING=true
fi
if hass.config.has_value 'relative_url'; then
export ESPHOME_DASHBOARD_RELATIVE_URL=$(hass.config.get 'relative_url')
fi
hass.log.info "Starting ESPHome dashboard..."
exec esphome /config/esphome dashboard --socket /var/run/esphome.sock --hassio

View File

@ -171,7 +171,7 @@ def compile_program(args, config):
def upload_using_esptool(config, port):
path = os.path.join(CORE.build_path, '.pioenvs', CORE.name, 'firmware.bin')
path = CORE.firmware_bin
cmd = ['esptool.py', '--before', 'default_reset', '--after', 'hard_reset',
'--chip', 'esp8266', '--port', port, 'write_flash', '0x0', path]

View File

@ -12,7 +12,7 @@ from typing import Any, Dict, List # noqa
from esphome.const import CONF_ARDUINO_VERSION, CONF_ESPHOME, CONF_ESPHOME_CORE_VERSION, \
CONF_LOCAL, CONF_USE_ADDRESS, CONF_WIFI, ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266, \
CONF_REPOSITORY, CONF_BRANCH
from esphome.helpers import ensure_unique_string
from esphome.helpers import ensure_unique_string, is_hassio
from esphome.py_compat import IS_PY2, integer_types
_LOGGER = logging.getLogger(__name__)
@ -358,9 +358,19 @@ class EsphomeCore(object):
path_ = os.path.expanduser(os.path.join(*path))
return os.path.join(self.build_path, path_)
def relative_pioenvs_path(self, *path):
if is_hassio():
return os.path.join('/data', self.name, '.pioenvs', *path)
return self.relative_build_path('.pioenvs', *path)
def relative_piolibdeps_path(self, *path):
if is_hassio():
return os.path.join('/data', self.name, '.piolibdeps', *path)
return self.relative_build_path('.piolibdeps', *path)
@property
def firmware_bin(self):
return self.relative_build_path('.pioenvs', self.name, 'firmware.bin')
return self.relative_pioenvs_path(self.name, 'firmware.bin')
@property
def is_esp8266(self):

View File

@ -2,9 +2,11 @@
from __future__ import print_function
import codecs
import collections
import hmac
import json
import logging
import multiprocessing
import os
import subprocess
import threading
@ -23,7 +25,7 @@ import tornado.websocket
from esphome import const
from esphome.__main__ import get_serial_ports
from esphome.helpers import mkdir_p
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
@ -42,6 +44,8 @@ USING_PASSWORD = False
ON_HASSIO = False
USING_HASSIO_AUTH = True
HASSIO_MQTT_CONFIG = None
RELATIVE_URL = os.getenv('ESPHOME_DASHBOARD_RELATIVE_URL', '/')
STATUS_USE_PING = get_bool_env('ESPHOME_DASHBOARD_USE_PING')
if IS_PY2:
cookie_authenticated_yes = 'yes'
@ -49,6 +53,17 @@ else:
cookie_authenticated_yes = b'yes'
def template_args():
version = const.__version__
return {
'version': version,
'docs_link': 'https://beta.esphome.io/' if 'b' in version else 'https://esphome.io/',
'get_static_file_url': get_static_file_url,
'relative_url': RELATIVE_URL,
'streamer_mode': get_bool_env('ESPHOME_STREAMER_MODE'),
}
# pylint: disable=abstract-method
class BaseHandler(tornado.web.RequestHandler):
def is_authenticated(self):
@ -165,7 +180,7 @@ class EsphomeHassConfigHandler(EsphomeCommandWebSocket):
class SerialPortRequestHandler(BaseHandler):
def get(self):
if not self.is_authenticated():
self.redirect('/login')
self.redirect(RELATIVE_URL + 'login')
return
ports = get_serial_ports()
data = []
@ -187,7 +202,7 @@ class WizardRequestHandler(BaseHandler):
from esphome import wizard
if not self.is_authenticated():
self.redirect('/login')
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')
@ -198,7 +213,7 @@ class WizardRequestHandler(BaseHandler):
class DownloadBinaryRequestHandler(BaseHandler):
def get(self):
if not self.is_authenticated():
self.redirect('/login')
self.redirect(RELATIVE_URL + 'login')
return
# pylint: disable=no-value-for-parameter
@ -214,8 +229,8 @@ class DownloadBinaryRequestHandler(BaseHandler):
filename = '{}.bin'.format(storage_json.name)
self.set_header("Content-Disposition", 'attachment; filename="{}"'.format(filename))
with open(path, 'rb') as f:
while 1:
data = f.read(16384) # or some other nice-sized chunk
while True:
data = f.read(16384)
if not data:
break
self.write(data)
@ -302,21 +317,26 @@ class DashboardEntry(object):
class MainRequestHandler(BaseHandler):
def get(self):
if not self.is_authenticated():
self.redirect('/login')
self.redirect(RELATIVE_URL + 'login')
return
begin = bool(self.get_argument('begin', False))
entries = _list_dashboard_entries()
version = const.__version__
docs_link = 'https://beta.esphome.io/' if 'b' in version else \
'https://esphome.io/'
self.render("templates/index.html", entries=entries,
version=version, begin=begin, docs_link=docs_link,
get_static_file_url=get_static_file_url)
self.render("templates/index.html", entries=entries, begin=begin,
**template_args())
class PingThread(threading.Thread):
def _ping_func(filename, address):
if os.name == 'nt':
command = ['ping', '-n', '1', address]
else:
command = ['ping', '-c', '1', address]
rc, _, _ = run_system_command(*command)
return filename, rc == 0
class MDNSStatusThread(threading.Thread):
def run(self):
zc = Zeroconf()
@ -337,10 +357,52 @@ class PingThread(threading.Thread):
zc.close()
class PingStatusThread(threading.Thread):
def run(self):
pool = multiprocessing.Pool(processes=8)
while not STOP_EVENT.is_set():
# Only do pings if somebody has the dashboard open
def callback(ret):
PING_RESULT[ret[0]] = ret[1]
entries = _list_dashboard_entries()
queue = collections.deque()
for entry in entries:
if entry.address is None:
PING_RESULT[entry.filename] = None
continue
result = pool.apply_async(_ping_func, (entry.filename, entry.address),
callback=callback)
queue.append(result)
while queue:
item = queue[0]
if item.ready():
queue.popleft()
continue
try:
item.get(0.1)
except OSError:
# ping not installed
pass
except multiprocessing.TimeoutError:
pass
if STOP_EVENT.is_set():
pool.terminate()
return
PING_REQUEST.wait()
PING_REQUEST.clear()
class PingRequestHandler(BaseHandler):
def get(self):
if not self.is_authenticated():
self.redirect('/login')
self.redirect(RELATIVE_URL + 'login')
return
PING_REQUEST.set()
@ -354,7 +416,7 @@ def is_allowed(configuration):
class EditRequestHandler(BaseHandler):
def get(self):
if not self.is_authenticated():
self.redirect('/login')
self.redirect(RELATIVE_URL + 'login')
return
# pylint: disable=no-value-for-parameter
configuration = self.get_argument('configuration')
@ -368,7 +430,7 @@ class EditRequestHandler(BaseHandler):
def post(self):
if not self.is_authenticated():
self.redirect('/login')
self.redirect(RELATIVE_URL + 'login')
return
# pylint: disable=no-value-for-parameter
configuration = self.get_argument('configuration')
@ -392,18 +454,13 @@ class LoginHandler(BaseHandler):
if USING_HASSIO_AUTH:
self.render_hassio_login()
return
self.write('<html><body><form action="/login" method="post">'
self.write('<html><body><form action="' + RELATIVE_URL + 'login" method="post">'
'Password: <input type="password" name="password">'
'<input type="submit" value="Sign in">'
'</form></body></html>')
def render_hassio_login(self, error=None):
version = const.__version__
docs_link = 'https://beta.esphome.io/' if 'b' in version else \
'https://esphome.io/'
self.render("templates/login.html", version=version, docs_link=docs_link, error=error,
get_static_file_url=get_static_file_url)
self.render("templates/login.html", error=error, **template_args())
def post_hassio_login(self):
import requests
@ -454,9 +511,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())
hash_ = hash(f_handle.read()) & (2**32-1)
_STATIC_FILE_HASHES[name] = hash_
return u'/static/{}?hash={}'.format(name, hash_)
return RELATIVE_URL + u'static/{}?hash={:08X}'.format(name, hash_)
def make_app(debug=False):
@ -491,21 +548,21 @@ def make_app(debug=False):
'websocket_ping_interval': 30.0,
}
app = tornado.web.Application([
(r"/", MainRequestHandler),
(r"/login", LoginHandler),
(r"/logs", EsphomeLogsHandler),
(r"/run", EsphomeRunHandler),
(r"/compile", EsphomeCompileHandler),
(r"/validate", EsphomeValidateHandler),
(r"/clean-mqtt", EsphomeCleanMqttHandler),
(r"/clean", EsphomeCleanHandler),
(r"/hass-config", EsphomeHassConfigHandler),
(r"/edit", EditRequestHandler),
(r"/download.bin", DownloadBinaryRequestHandler),
(r"/serial-ports", SerialPortRequestHandler),
(r"/ping", PingRequestHandler),
(r"/wizard.html", WizardRequestHandler),
(r'/static/(.*)', StaticFileHandler, {'path': static_path}),
(RELATIVE_URL + "", MainRequestHandler),
(RELATIVE_URL + "login", LoginHandler),
(RELATIVE_URL + "logs", EsphomeLogsHandler),
(RELATIVE_URL + "run", EsphomeRunHandler),
(RELATIVE_URL + "compile", EsphomeCompileHandler),
(RELATIVE_URL + "validate", EsphomeValidateHandler),
(RELATIVE_URL + "clean-mqtt", EsphomeCleanMqttHandler),
(RELATIVE_URL + "clean", EsphomeCleanHandler),
(RELATIVE_URL + "hass-config", EsphomeHassConfigHandler),
(RELATIVE_URL + "edit", EditRequestHandler),
(RELATIVE_URL + "download.bin", DownloadBinaryRequestHandler),
(RELATIVE_URL + "serial-ports", SerialPortRequestHandler),
(RELATIVE_URL + "ping", PingRequestHandler),
(RELATIVE_URL + "wizard.html", WizardRequestHandler),
(RELATIVE_URL + r"static/(.*)", StaticFileHandler, {'path': static_path}),
], **settings)
if debug:
@ -528,7 +585,7 @@ def start_web_server(args):
ON_HASSIO = args.hassio
if ON_HASSIO:
USING_HASSIO_AUTH = not bool(os.getenv('DISABLE_HA_AUTHENTICATION'))
USING_HASSIO_AUTH = not get_bool_env('DISABLE_HA_AUTHENTICATION')
USING_PASSWORD = False
else:
USING_HASSIO_AUTH = False
@ -565,14 +622,17 @@ def start_web_server(args):
webbrowser.open('localhost:{}'.format(args.port))
ping_thread = PingThread()
ping_thread.start()
if STATUS_USE_PING:
status_thread = PingStatusThread()
else:
status_thread = MDNSStatusThread()
status_thread.start()
try:
tornado.ioloop.IOLoop.current().start()
except KeyboardInterrupt:
_LOGGER.info("Shutting down...")
STOP_EVENT.set()
PING_REQUEST.set()
ping_thread.join()
status_thread.join()
if args.socket is not None:
os.remove(args.socket)

View File

@ -1,3 +1,5 @@
// Disclaimer: This file was written in a hurry and by someone
// who does not know JS at all. This file desperately needs cleanup.
document.addEventListener('DOMContentLoaded', () => {
M.AutoInit(document.body);
});
@ -183,7 +185,7 @@ let wsProtocol = "ws:";
if (window.location.protocol === "https:") {
wsProtocol = 'wss:';
}
const wsUrl = wsProtocol + '//' + window.location.hostname + ':' + window.location.port;
const wsUrl = `${wsProtocol}//${window.location.hostname}:${window.location.port}${relative_url}`;
let isFetchingPing = false;
const fetchPing = () => {
@ -191,7 +193,7 @@ const fetchPing = () => {
return;
isFetchingPing = true;
fetch('/ping', {credentials: "same-origin"}).then(res => res.json())
fetch(`${relative_url}ping`, {credentials: "same-origin"}).then(res => res.json())
.then(response => {
for (let filename in response) {
let node = document.querySelector(`.status-indicator[data-node="${filename}"]`);
@ -233,7 +235,7 @@ const portSelect = document.querySelector('.nav-wrapper select');
let ports = [];
const fetchSerialPorts = (begin=false) => {
fetch('/serial-ports', {credentials: "same-origin"}).then(res => res.json())
fetch(`${relative_url}serial-ports`, {credentials: "same-origin"}).then(res => res.json())
.then(response => {
if (ports.length === response.length) {
let allEqual = true;
@ -301,7 +303,7 @@ document.querySelectorAll(".action-show-logs").forEach((showLogs) => {
const filenameField = logsModalElem.querySelector('.filename');
filenameField.innerHTML = configuration;
const logSocket = new WebSocket(wsUrl + "/logs");
const logSocket = new WebSocket(wsUrl + "logs");
logSocket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
if (data.event === "line") {
@ -350,7 +352,7 @@ document.querySelectorAll(".action-upload").forEach((upload) => {
const filenameField = uploadModalElem.querySelector('.filename');
filenameField.innerHTML = configuration;
const logSocket = new WebSocket(wsUrl + "/run");
const logSocket = new WebSocket(wsUrl + "run");
logSocket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
if (data.event === "line") {
@ -399,7 +401,7 @@ document.querySelectorAll(".action-validate").forEach((upload) => {
const filenameField = validateModalElem.querySelector('.filename');
filenameField.innerHTML = configuration;
const logSocket = new WebSocket(wsUrl + "/validate");
const logSocket = new WebSocket(wsUrl + "validate");
logSocket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
if (data.event === "line") {
@ -457,7 +459,7 @@ document.querySelectorAll(".action-compile").forEach((upload) => {
const filenameField = compileModalElem.querySelector('.filename');
filenameField.innerHTML = configuration;
const logSocket = new WebSocket(wsUrl + "/compile");
const logSocket = new WebSocket(wsUrl + "compile");
logSocket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
if (data.event === "line") {
@ -492,7 +494,7 @@ document.querySelectorAll(".action-compile").forEach((upload) => {
downloadButton.addEventListener('click', () => {
const link = document.createElement("a");
link.download = name;
link.href = '/download.bin?configuration=' + encodeURIComponent(configuration);
link.href = `${relative_url}download.bin?configuration=${encodeURIComponent(configuration)}`;
document.body.appendChild(link);
link.click();
link.remove();
@ -515,7 +517,7 @@ document.querySelectorAll(".action-clean-mqtt").forEach((btn) => {
const filenameField = cleanMqttModalElem.querySelector('.filename');
filenameField.innerHTML = configuration;
const logSocket = new WebSocket(wsUrl + "/clean-mqtt");
const logSocket = new WebSocket(wsUrl + "clean-mqtt");
logSocket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
if (data.event === "line") {
@ -557,7 +559,7 @@ document.querySelectorAll(".action-clean").forEach((btn) => {
const filenameField = cleanModalElem.querySelector('.filename');
filenameField.innerHTML = configuration;
const logSocket = new WebSocket(wsUrl + "/clean");
const logSocket = new WebSocket(wsUrl + "clean");
logSocket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
if (data.event === "line") {
@ -605,7 +607,7 @@ document.querySelectorAll(".action-hass-config").forEach((btn) => {
const filenameField = hassConfigModalElem.querySelector('.filename');
filenameField.innerHTML = configuration;
const logSocket = new WebSocket(wsUrl + "/hass-config");
const logSocket = new WebSocket(wsUrl + "hass-config");
logSocket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
if (data.event === "line") {
@ -646,7 +648,7 @@ editor.session.setOption('tabSize', 2);
const saveButton = editModalElem.querySelector(".save-button");
const saveEditor = () => {
fetch(`/edit?configuration=${configuration}`, {
fetch(`${relative_url}edit?configuration=${configuration}`, {
credentials: "same-origin",
method: "POST",
body: editor.getValue()
@ -673,7 +675,7 @@ document.querySelectorAll(".action-edit").forEach((btn) => {
const filenameField = editModalElem.querySelector('.filename');
filenameField.innerHTML = configuration;
fetch(`/edit?configuration=${configuration}`, {credentials: "same-origin"})
fetch(`${relative_url}edit?configuration=${configuration}`, {credentials: "same-origin"})
.then(res => res.text()).then(response => {
editor.setValue(response, -1);
});

View File

@ -16,6 +16,15 @@
<script src="{{ get_static_file_url('materialize-stepper.min.js') }}"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<script>const relative_url = "{{ relative_url }}";</script>
{% if streamer_mode %}
<style>
.log-secret {
visibility: hidden !important;
}
</style>
{% end %}
</head>
<body>

View File

@ -28,7 +28,7 @@
<div class="container">
<div class="row">
<div class="col card s10 offset-s1 m10 offset-m1 l8 offset-l2">
<form action="/login" method="post">
<form action="{{ relative_url }}login" method="post">
<div class="card-content">
<span class="card-title">Enter credentials</span>
<p>

View File

@ -134,6 +134,14 @@ def resolve_ip_address(host):
return ip
def get_bool_env(var, default=False):
return bool(os.getenv(var, default))
def is_hassio():
return get_bool_env('ESPHOME_IS_HASSIO')
def symlink(src, dst):
if hasattr(os, 'symlink'):
os.symlink(src, dst)

View File

@ -14,6 +14,8 @@ _LOGGER = logging.getLogger(__name__)
def run_platformio_cli(*args, **kwargs):
os.environ["PLATFORMIO_FORCE_COLOR"] = "true"
os.environ["PLATFORMIO_BUILD_DIR"] = os.path.abspath(CORE.relative_pioenvs_path())
os.environ["PLATFORMIO_LIBDEPS_DIR"] = os.path.abspath(CORE.relative_piolibdeps_path())
cmd = ['platformio'] + list(args)
if os.environ.get('ESPHOME_USE_SUBPROCESS') is None:

View File

@ -8,7 +8,7 @@ import voluptuous as vol
import esphome.config_validation as cv
from esphome.const import ESP_PLATFORMS, ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266
from esphome.helpers import color
from esphome.helpers import color, get_bool_env
# pylint: disable=anomalous-backslash-in-string
from esphome.pins import ESP32_BOARD_PINS, ESP8266_BOARD_PINS
from esphome.py_compat import safe_input, text_type
@ -86,7 +86,7 @@ def wizard_write(path, **kwargs):
storage.save(storage_path)
if os.getenv('ESPHOME_QUICKWIZARD', ''):
if get_bool_env('ESPHOME_QUICKWIZARD'):
def sleep(time):
pass
else:

View File

@ -105,7 +105,7 @@ def update_esphome_core_repo():
# Git commit hash or tag cannot be updated
return
esphome_core_path = CORE.relative_build_path('.piolibdeps', 'esphome-core')
esphome_core_path = CORE.relative_piolibdeps_path('esphome-core')
rc, _, _ = run_system_command('git', '-C', esphome_core_path, '--help')
if rc != 0:
@ -503,12 +503,14 @@ def write_cpp(code_s):
def clean_build():
for directory in ('.piolibdeps', '.pioenvs'):
dir_path = CORE.relative_build_path(directory)
if not os.path.isdir(dir_path):
continue
_LOGGER.info("Deleting %s", dir_path)
shutil.rmtree(dir_path)
pioenvs = CORE.relative_pioenvs_path()
if os.path.isdir(pioenvs):
_LOGGER.info("Deleting %s", pioenvs)
shutil.rmtree(pioenvs)
piolibdeps = CORE.relative_piolibdeps_path()
if os.path.isdir(piolibdeps):
_LOGGER.info("Deleting %s", piolibdeps)
shutil.rmtree(piolibdeps)
GITIGNORE_CONTENT = """# Gitignore settings for ESPHome