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 # 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

View file

@ -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]

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, \ 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):

View file

@ -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)

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', () => { 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);
}); });

View file

@ -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>

View file

@ -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>

View file

@ -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)

View file

@ -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:

View file

@ -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:

View file

@ -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