mirror of
https://github.com/esphome/esphome.git
synced 2024-11-29 02:04:13 +01:00
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:
parent
5a102c2ab7
commit
067ec30c56
11 changed files with 178 additions and 71 deletions
|
@ -6,9 +6,23 @@
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
source /usr/lib/hassio-addons/base.sh
|
source /usr/lib/hassio-addons/base.sh
|
||||||
|
|
||||||
|
export ESPHOME_IS_HASSIO=true
|
||||||
|
|
||||||
if hass.config.true 'leave_front_door_open'; then
|
if hass.config.true 'leave_front_door_open'; then
|
||||||
export DISABLE_HA_AUTHENTICATION=true
|
export DISABLE_HA_AUTHENTICATION=true
|
||||||
fi
|
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..."
|
hass.log.info "Starting ESPHome dashboard..."
|
||||||
exec esphome /config/esphome dashboard --socket /var/run/esphome.sock --hassio
|
exec esphome /config/esphome dashboard --socket /var/run/esphome.sock --hassio
|
||||||
|
|
|
@ -171,7 +171,7 @@ def compile_program(args, config):
|
||||||
|
|
||||||
|
|
||||||
def upload_using_esptool(config, port):
|
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',
|
cmd = ['esptool.py', '--before', 'default_reset', '--after', 'hard_reset',
|
||||||
'--chip', 'esp8266', '--port', port, 'write_flash', '0x0', path]
|
'--chip', 'esp8266', '--port', port, 'write_flash', '0x0', path]
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ from typing import Any, Dict, List # noqa
|
||||||
from esphome.const import CONF_ARDUINO_VERSION, CONF_ESPHOME, CONF_ESPHOME_CORE_VERSION, \
|
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_LOCAL, CONF_USE_ADDRESS, CONF_WIFI, ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266, \
|
||||||
CONF_REPOSITORY, CONF_BRANCH
|
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
|
from esphome.py_compat import IS_PY2, integer_types
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -358,9 +358,19 @@ class EsphomeCore(object):
|
||||||
path_ = os.path.expanduser(os.path.join(*path))
|
path_ = os.path.expanduser(os.path.join(*path))
|
||||||
return os.path.join(self.build_path, 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
|
@property
|
||||||
def firmware_bin(self):
|
def firmware_bin(self):
|
||||||
return self.relative_build_path('.pioenvs', self.name, 'firmware.bin')
|
return self.relative_pioenvs_path(self.name, 'firmware.bin')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_esp8266(self):
|
def is_esp8266(self):
|
||||||
|
|
|
@ -2,9 +2,11 @@
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
import codecs
|
import codecs
|
||||||
|
import collections
|
||||||
import hmac
|
import hmac
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import multiprocessing
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
|
@ -23,7 +25,7 @@ import tornado.websocket
|
||||||
|
|
||||||
from esphome import const
|
from esphome import const
|
||||||
from esphome.__main__ import get_serial_ports
|
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.py_compat import IS_PY2
|
||||||
from esphome.storage_json import EsphomeStorageJSON, StorageJSON, \
|
from esphome.storage_json import EsphomeStorageJSON, StorageJSON, \
|
||||||
esphome_storage_path, ext_storage_path
|
esphome_storage_path, ext_storage_path
|
||||||
|
@ -42,6 +44,8 @@ USING_PASSWORD = False
|
||||||
ON_HASSIO = False
|
ON_HASSIO = False
|
||||||
USING_HASSIO_AUTH = True
|
USING_HASSIO_AUTH = True
|
||||||
HASSIO_MQTT_CONFIG = None
|
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:
|
if IS_PY2:
|
||||||
cookie_authenticated_yes = 'yes'
|
cookie_authenticated_yes = 'yes'
|
||||||
|
@ -49,6 +53,17 @@ else:
|
||||||
cookie_authenticated_yes = b'yes'
|
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
|
# pylint: disable=abstract-method
|
||||||
class BaseHandler(tornado.web.RequestHandler):
|
class BaseHandler(tornado.web.RequestHandler):
|
||||||
def is_authenticated(self):
|
def is_authenticated(self):
|
||||||
|
@ -165,7 +180,7 @@ class EsphomeHassConfigHandler(EsphomeCommandWebSocket):
|
||||||
class SerialPortRequestHandler(BaseHandler):
|
class SerialPortRequestHandler(BaseHandler):
|
||||||
def get(self):
|
def get(self):
|
||||||
if not self.is_authenticated():
|
if not self.is_authenticated():
|
||||||
self.redirect('/login')
|
self.redirect(RELATIVE_URL + 'login')
|
||||||
return
|
return
|
||||||
ports = get_serial_ports()
|
ports = get_serial_ports()
|
||||||
data = []
|
data = []
|
||||||
|
@ -187,7 +202,7 @@ class WizardRequestHandler(BaseHandler):
|
||||||
from esphome import wizard
|
from esphome import wizard
|
||||||
|
|
||||||
if not self.is_authenticated():
|
if not self.is_authenticated():
|
||||||
self.redirect('/login')
|
self.redirect(RELATIVE_URL + 'login')
|
||||||
return
|
return
|
||||||
kwargs = {k: ''.join(v) for k, v in self.request.arguments.items()}
|
kwargs = {k: ''.join(v) for k, v in self.request.arguments.items()}
|
||||||
destination = os.path.join(CONFIG_DIR, kwargs['name'] + '.yaml')
|
destination = os.path.join(CONFIG_DIR, kwargs['name'] + '.yaml')
|
||||||
|
@ -198,7 +213,7 @@ class WizardRequestHandler(BaseHandler):
|
||||||
class DownloadBinaryRequestHandler(BaseHandler):
|
class DownloadBinaryRequestHandler(BaseHandler):
|
||||||
def get(self):
|
def get(self):
|
||||||
if not self.is_authenticated():
|
if not self.is_authenticated():
|
||||||
self.redirect('/login')
|
self.redirect(RELATIVE_URL + 'login')
|
||||||
return
|
return
|
||||||
|
|
||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
|
@ -214,8 +229,8 @@ class DownloadBinaryRequestHandler(BaseHandler):
|
||||||
filename = '{}.bin'.format(storage_json.name)
|
filename = '{}.bin'.format(storage_json.name)
|
||||||
self.set_header("Content-Disposition", 'attachment; filename="{}"'.format(filename))
|
self.set_header("Content-Disposition", 'attachment; filename="{}"'.format(filename))
|
||||||
with open(path, 'rb') as f:
|
with open(path, 'rb') as f:
|
||||||
while 1:
|
while True:
|
||||||
data = f.read(16384) # or some other nice-sized chunk
|
data = f.read(16384)
|
||||||
if not data:
|
if not data:
|
||||||
break
|
break
|
||||||
self.write(data)
|
self.write(data)
|
||||||
|
@ -302,21 +317,26 @@ class DashboardEntry(object):
|
||||||
class MainRequestHandler(BaseHandler):
|
class MainRequestHandler(BaseHandler):
|
||||||
def get(self):
|
def get(self):
|
||||||
if not self.is_authenticated():
|
if not self.is_authenticated():
|
||||||
self.redirect('/login')
|
self.redirect(RELATIVE_URL + 'login')
|
||||||
return
|
return
|
||||||
|
|
||||||
begin = bool(self.get_argument('begin', False))
|
begin = bool(self.get_argument('begin', False))
|
||||||
entries = _list_dashboard_entries()
|
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,
|
self.render("templates/index.html", entries=entries, begin=begin,
|
||||||
version=version, begin=begin, docs_link=docs_link,
|
**template_args())
|
||||||
get_static_file_url=get_static_file_url)
|
|
||||||
|
|
||||||
|
|
||||||
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):
|
def run(self):
|
||||||
zc = Zeroconf()
|
zc = Zeroconf()
|
||||||
|
|
||||||
|
@ -337,10 +357,52 @@ class PingThread(threading.Thread):
|
||||||
zc.close()
|
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):
|
class PingRequestHandler(BaseHandler):
|
||||||
def get(self):
|
def get(self):
|
||||||
if not self.is_authenticated():
|
if not self.is_authenticated():
|
||||||
self.redirect('/login')
|
self.redirect(RELATIVE_URL + 'login')
|
||||||
return
|
return
|
||||||
|
|
||||||
PING_REQUEST.set()
|
PING_REQUEST.set()
|
||||||
|
@ -354,7 +416,7 @@ def is_allowed(configuration):
|
||||||
class EditRequestHandler(BaseHandler):
|
class EditRequestHandler(BaseHandler):
|
||||||
def get(self):
|
def get(self):
|
||||||
if not self.is_authenticated():
|
if not self.is_authenticated():
|
||||||
self.redirect('/login')
|
self.redirect(RELATIVE_URL + 'login')
|
||||||
return
|
return
|
||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
configuration = self.get_argument('configuration')
|
configuration = self.get_argument('configuration')
|
||||||
|
@ -368,7 +430,7 @@ class EditRequestHandler(BaseHandler):
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
if not self.is_authenticated():
|
if not self.is_authenticated():
|
||||||
self.redirect('/login')
|
self.redirect(RELATIVE_URL + 'login')
|
||||||
return
|
return
|
||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
configuration = self.get_argument('configuration')
|
configuration = self.get_argument('configuration')
|
||||||
|
@ -392,18 +454,13 @@ class LoginHandler(BaseHandler):
|
||||||
if USING_HASSIO_AUTH:
|
if USING_HASSIO_AUTH:
|
||||||
self.render_hassio_login()
|
self.render_hassio_login()
|
||||||
return
|
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">'
|
'Password: <input type="password" name="password">'
|
||||||
'<input type="submit" value="Sign in">'
|
'<input type="submit" value="Sign in">'
|
||||||
'</form></body></html>')
|
'</form></body></html>')
|
||||||
|
|
||||||
def render_hassio_login(self, error=None):
|
def render_hassio_login(self, error=None):
|
||||||
version = const.__version__
|
self.render("templates/login.html", error=error, **template_args())
|
||||||
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)
|
|
||||||
|
|
||||||
def post_hassio_login(self):
|
def post_hassio_login(self):
|
||||||
import requests
|
import requests
|
||||||
|
@ -454,9 +511,9 @@ def get_static_file_url(name):
|
||||||
else:
|
else:
|
||||||
path = os.path.join(static_path, name)
|
path = os.path.join(static_path, name)
|
||||||
with open(path, 'rb') as f_handle:
|
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_
|
_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):
|
def make_app(debug=False):
|
||||||
|
@ -491,21 +548,21 @@ def make_app(debug=False):
|
||||||
'websocket_ping_interval': 30.0,
|
'websocket_ping_interval': 30.0,
|
||||||
}
|
}
|
||||||
app = tornado.web.Application([
|
app = tornado.web.Application([
|
||||||
(r"/", MainRequestHandler),
|
(RELATIVE_URL + "", MainRequestHandler),
|
||||||
(r"/login", LoginHandler),
|
(RELATIVE_URL + "login", LoginHandler),
|
||||||
(r"/logs", EsphomeLogsHandler),
|
(RELATIVE_URL + "logs", EsphomeLogsHandler),
|
||||||
(r"/run", EsphomeRunHandler),
|
(RELATIVE_URL + "run", EsphomeRunHandler),
|
||||||
(r"/compile", EsphomeCompileHandler),
|
(RELATIVE_URL + "compile", EsphomeCompileHandler),
|
||||||
(r"/validate", EsphomeValidateHandler),
|
(RELATIVE_URL + "validate", EsphomeValidateHandler),
|
||||||
(r"/clean-mqtt", EsphomeCleanMqttHandler),
|
(RELATIVE_URL + "clean-mqtt", EsphomeCleanMqttHandler),
|
||||||
(r"/clean", EsphomeCleanHandler),
|
(RELATIVE_URL + "clean", EsphomeCleanHandler),
|
||||||
(r"/hass-config", EsphomeHassConfigHandler),
|
(RELATIVE_URL + "hass-config", EsphomeHassConfigHandler),
|
||||||
(r"/edit", EditRequestHandler),
|
(RELATIVE_URL + "edit", EditRequestHandler),
|
||||||
(r"/download.bin", DownloadBinaryRequestHandler),
|
(RELATIVE_URL + "download.bin", DownloadBinaryRequestHandler),
|
||||||
(r"/serial-ports", SerialPortRequestHandler),
|
(RELATIVE_URL + "serial-ports", SerialPortRequestHandler),
|
||||||
(r"/ping", PingRequestHandler),
|
(RELATIVE_URL + "ping", PingRequestHandler),
|
||||||
(r"/wizard.html", WizardRequestHandler),
|
(RELATIVE_URL + "wizard.html", WizardRequestHandler),
|
||||||
(r'/static/(.*)', StaticFileHandler, {'path': static_path}),
|
(RELATIVE_URL + r"static/(.*)", StaticFileHandler, {'path': static_path}),
|
||||||
], **settings)
|
], **settings)
|
||||||
|
|
||||||
if debug:
|
if debug:
|
||||||
|
@ -528,7 +585,7 @@ def start_web_server(args):
|
||||||
|
|
||||||
ON_HASSIO = args.hassio
|
ON_HASSIO = args.hassio
|
||||||
if ON_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
|
USING_PASSWORD = False
|
||||||
else:
|
else:
|
||||||
USING_HASSIO_AUTH = False
|
USING_HASSIO_AUTH = False
|
||||||
|
@ -565,14 +622,17 @@ def start_web_server(args):
|
||||||
|
|
||||||
webbrowser.open('localhost:{}'.format(args.port))
|
webbrowser.open('localhost:{}'.format(args.port))
|
||||||
|
|
||||||
ping_thread = PingThread()
|
if STATUS_USE_PING:
|
||||||
ping_thread.start()
|
status_thread = PingStatusThread()
|
||||||
|
else:
|
||||||
|
status_thread = MDNSStatusThread()
|
||||||
|
status_thread.start()
|
||||||
try:
|
try:
|
||||||
tornado.ioloop.IOLoop.current().start()
|
tornado.ioloop.IOLoop.current().start()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
_LOGGER.info("Shutting down...")
|
_LOGGER.info("Shutting down...")
|
||||||
STOP_EVENT.set()
|
STOP_EVENT.set()
|
||||||
PING_REQUEST.set()
|
PING_REQUEST.set()
|
||||||
ping_thread.join()
|
status_thread.join()
|
||||||
if args.socket is not None:
|
if args.socket is not None:
|
||||||
os.remove(args.socket)
|
os.remove(args.socket)
|
||||||
|
|
|
@ -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', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
M.AutoInit(document.body);
|
M.AutoInit(document.body);
|
||||||
});
|
});
|
||||||
|
@ -183,7 +185,7 @@ let wsProtocol = "ws:";
|
||||||
if (window.location.protocol === "https:") {
|
if (window.location.protocol === "https:") {
|
||||||
wsProtocol = 'wss:';
|
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;
|
let isFetchingPing = false;
|
||||||
const fetchPing = () => {
|
const fetchPing = () => {
|
||||||
|
@ -191,7 +193,7 @@ const fetchPing = () => {
|
||||||
return;
|
return;
|
||||||
isFetchingPing = true;
|
isFetchingPing = true;
|
||||||
|
|
||||||
fetch('/ping', {credentials: "same-origin"}).then(res => res.json())
|
fetch(`${relative_url}ping`, {credentials: "same-origin"}).then(res => res.json())
|
||||||
.then(response => {
|
.then(response => {
|
||||||
for (let filename in response) {
|
for (let filename in response) {
|
||||||
let node = document.querySelector(`.status-indicator[data-node="${filename}"]`);
|
let node = document.querySelector(`.status-indicator[data-node="${filename}"]`);
|
||||||
|
@ -233,7 +235,7 @@ const portSelect = document.querySelector('.nav-wrapper select');
|
||||||
let ports = [];
|
let ports = [];
|
||||||
|
|
||||||
const fetchSerialPorts = (begin=false) => {
|
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 => {
|
.then(response => {
|
||||||
if (ports.length === response.length) {
|
if (ports.length === response.length) {
|
||||||
let allEqual = true;
|
let allEqual = true;
|
||||||
|
@ -301,7 +303,7 @@ document.querySelectorAll(".action-show-logs").forEach((showLogs) => {
|
||||||
const filenameField = logsModalElem.querySelector('.filename');
|
const filenameField = logsModalElem.querySelector('.filename');
|
||||||
filenameField.innerHTML = configuration;
|
filenameField.innerHTML = configuration;
|
||||||
|
|
||||||
const logSocket = new WebSocket(wsUrl + "/logs");
|
const logSocket = new WebSocket(wsUrl + "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") {
|
||||||
|
@ -350,7 +352,7 @@ document.querySelectorAll(".action-upload").forEach((upload) => {
|
||||||
const filenameField = uploadModalElem.querySelector('.filename');
|
const filenameField = uploadModalElem.querySelector('.filename');
|
||||||
filenameField.innerHTML = configuration;
|
filenameField.innerHTML = configuration;
|
||||||
|
|
||||||
const logSocket = new WebSocket(wsUrl + "/run");
|
const logSocket = new WebSocket(wsUrl + "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") {
|
||||||
|
@ -399,7 +401,7 @@ document.querySelectorAll(".action-validate").forEach((upload) => {
|
||||||
const filenameField = validateModalElem.querySelector('.filename');
|
const filenameField = validateModalElem.querySelector('.filename');
|
||||||
filenameField.innerHTML = configuration;
|
filenameField.innerHTML = configuration;
|
||||||
|
|
||||||
const logSocket = new WebSocket(wsUrl + "/validate");
|
const logSocket = new WebSocket(wsUrl + "validate");
|
||||||
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") {
|
||||||
|
@ -457,7 +459,7 @@ document.querySelectorAll(".action-compile").forEach((upload) => {
|
||||||
const filenameField = compileModalElem.querySelector('.filename');
|
const filenameField = compileModalElem.querySelector('.filename');
|
||||||
filenameField.innerHTML = configuration;
|
filenameField.innerHTML = configuration;
|
||||||
|
|
||||||
const logSocket = new WebSocket(wsUrl + "/compile");
|
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") {
|
||||||
|
@ -492,7 +494,7 @@ document.querySelectorAll(".action-compile").forEach((upload) => {
|
||||||
downloadButton.addEventListener('click', () => {
|
downloadButton.addEventListener('click', () => {
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.download = name;
|
link.download = name;
|
||||||
link.href = '/download.bin?configuration=' + encodeURIComponent(configuration);
|
link.href = `${relative_url}download.bin?configuration=${encodeURIComponent(configuration)}`;
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
link.remove();
|
link.remove();
|
||||||
|
@ -515,7 +517,7 @@ document.querySelectorAll(".action-clean-mqtt").forEach((btn) => {
|
||||||
const filenameField = cleanMqttModalElem.querySelector('.filename');
|
const filenameField = cleanMqttModalElem.querySelector('.filename');
|
||||||
filenameField.innerHTML = configuration;
|
filenameField.innerHTML = configuration;
|
||||||
|
|
||||||
const logSocket = new WebSocket(wsUrl + "/clean-mqtt");
|
const logSocket = new WebSocket(wsUrl + "clean-mqtt");
|
||||||
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") {
|
||||||
|
@ -557,7 +559,7 @@ document.querySelectorAll(".action-clean").forEach((btn) => {
|
||||||
const filenameField = cleanModalElem.querySelector('.filename');
|
const filenameField = cleanModalElem.querySelector('.filename');
|
||||||
filenameField.innerHTML = configuration;
|
filenameField.innerHTML = configuration;
|
||||||
|
|
||||||
const logSocket = new WebSocket(wsUrl + "/clean");
|
const logSocket = new WebSocket(wsUrl + "clean");
|
||||||
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") {
|
||||||
|
@ -605,7 +607,7 @@ document.querySelectorAll(".action-hass-config").forEach((btn) => {
|
||||||
const filenameField = hassConfigModalElem.querySelector('.filename');
|
const filenameField = hassConfigModalElem.querySelector('.filename');
|
||||||
filenameField.innerHTML = configuration;
|
filenameField.innerHTML = configuration;
|
||||||
|
|
||||||
const logSocket = new WebSocket(wsUrl + "/hass-config");
|
const logSocket = new WebSocket(wsUrl + "hass-config");
|
||||||
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") {
|
||||||
|
@ -646,7 +648,7 @@ editor.session.setOption('tabSize', 2);
|
||||||
|
|
||||||
const saveButton = editModalElem.querySelector(".save-button");
|
const saveButton = editModalElem.querySelector(".save-button");
|
||||||
const saveEditor = () => {
|
const saveEditor = () => {
|
||||||
fetch(`/edit?configuration=${configuration}`, {
|
fetch(`${relative_url}edit?configuration=${configuration}`, {
|
||||||
credentials: "same-origin",
|
credentials: "same-origin",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: editor.getValue()
|
body: editor.getValue()
|
||||||
|
@ -673,7 +675,7 @@ document.querySelectorAll(".action-edit").forEach((btn) => {
|
||||||
const filenameField = editModalElem.querySelector('.filename');
|
const filenameField = editModalElem.querySelector('.filename');
|
||||||
filenameField.innerHTML = configuration;
|
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 => {
|
.then(res => res.text()).then(response => {
|
||||||
editor.setValue(response, -1);
|
editor.setValue(response, -1);
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,6 +16,15 @@
|
||||||
|
|
||||||
<script src="{{ get_static_file_url('materialize-stepper.min.js') }}"></script>
|
<script src="{{ get_static_file_url('materialize-stepper.min.js') }}"></script>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col card s10 offset-s1 m10 offset-m1 l8 offset-l2">
|
<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">
|
<div class="card-content">
|
||||||
<span class="card-title">Enter credentials</span>
|
<span class="card-title">Enter credentials</span>
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -134,6 +134,14 @@ def resolve_ip_address(host):
|
||||||
return ip
|
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):
|
def symlink(src, dst):
|
||||||
if hasattr(os, 'symlink'):
|
if hasattr(os, 'symlink'):
|
||||||
os.symlink(src, dst)
|
os.symlink(src, dst)
|
||||||
|
|
|
@ -14,6 +14,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
def run_platformio_cli(*args, **kwargs):
|
def run_platformio_cli(*args, **kwargs):
|
||||||
os.environ["PLATFORMIO_FORCE_COLOR"] = "true"
|
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)
|
cmd = ['platformio'] + list(args)
|
||||||
|
|
||||||
if os.environ.get('ESPHOME_USE_SUBPROCESS') is None:
|
if os.environ.get('ESPHOME_USE_SUBPROCESS') is None:
|
||||||
|
|
|
@ -8,7 +8,7 @@ import voluptuous as vol
|
||||||
|
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import ESP_PLATFORMS, ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266
|
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
|
# pylint: disable=anomalous-backslash-in-string
|
||||||
from esphome.pins import ESP32_BOARD_PINS, ESP8266_BOARD_PINS
|
from esphome.pins import ESP32_BOARD_PINS, ESP8266_BOARD_PINS
|
||||||
from esphome.py_compat import safe_input, text_type
|
from esphome.py_compat import safe_input, text_type
|
||||||
|
@ -86,7 +86,7 @@ def wizard_write(path, **kwargs):
|
||||||
storage.save(storage_path)
|
storage.save(storage_path)
|
||||||
|
|
||||||
|
|
||||||
if os.getenv('ESPHOME_QUICKWIZARD', ''):
|
if get_bool_env('ESPHOME_QUICKWIZARD'):
|
||||||
def sleep(time):
|
def sleep(time):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -105,7 +105,7 @@ def update_esphome_core_repo():
|
||||||
# Git commit hash or tag cannot be updated
|
# Git commit hash or tag cannot be updated
|
||||||
return
|
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')
|
rc, _, _ = run_system_command('git', '-C', esphome_core_path, '--help')
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
|
@ -503,12 +503,14 @@ def write_cpp(code_s):
|
||||||
|
|
||||||
|
|
||||||
def clean_build():
|
def clean_build():
|
||||||
for directory in ('.piolibdeps', '.pioenvs'):
|
pioenvs = CORE.relative_pioenvs_path()
|
||||||
dir_path = CORE.relative_build_path(directory)
|
if os.path.isdir(pioenvs):
|
||||||
if not os.path.isdir(dir_path):
|
_LOGGER.info("Deleting %s", pioenvs)
|
||||||
continue
|
shutil.rmtree(pioenvs)
|
||||||
_LOGGER.info("Deleting %s", dir_path)
|
piolibdeps = CORE.relative_piolibdeps_path()
|
||||||
shutil.rmtree(dir_path)
|
if os.path.isdir(piolibdeps):
|
||||||
|
_LOGGER.info("Deleting %s", piolibdeps)
|
||||||
|
shutil.rmtree(piolibdeps)
|
||||||
|
|
||||||
|
|
||||||
GITIGNORE_CONTENT = """# Gitignore settings for ESPHome
|
GITIGNORE_CONTENT = """# Gitignore settings for ESPHome
|
||||||
|
|
Loading…
Reference in a new issue