mirror of
https://github.com/esphome/esphome.git
synced 2024-12-24 14:34:54 +01:00
1148 lines
40 KiB
HTML
1148 lines
40 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>esphomeyaml Dashboard</title>
|
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/css/materialize.min.css">
|
|
|
|
<link rel="stylesheet" href="/static/materialize-stepper.min.css">
|
|
|
|
<!-- jQuery :( -->
|
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
|
|
<script src="https://code.jquery.com/ui/1.8.5/jquery-ui.min.js" integrity="sha256-fOse6WapxTrUSJOJICXXYwHRJOPa6C1OUQXi7C9Ddy8=" crossorigin="anonymous"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/js/materialize.min.js"></script>
|
|
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.15.0/jquery.validate.min.js"></script>
|
|
|
|
|
|
<script src="/static/materialize-stepper.min.js"></script>
|
|
|
|
<link rel="shortcut icon" href="/static/favicon.ico">
|
|
|
|
<style>
|
|
nav .brand-logo {
|
|
margin-left: 48px;
|
|
font-size: 20px;
|
|
}
|
|
|
|
main .container {
|
|
margin-top: -12vh;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.ribbon {
|
|
width: 100%;
|
|
height: 17vh;
|
|
background-color: #3F51B5;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.ribbon-fab:not(.tap-target-origin) {
|
|
position: absolute;
|
|
right: 24px;
|
|
top: calc(17vh + 34px);
|
|
}
|
|
|
|
i.very-large {
|
|
font-size: 8rem;
|
|
padding-top: 2px;
|
|
color: #424242;
|
|
}
|
|
|
|
.card .card-content {
|
|
padding-left: 18px;
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
.log {
|
|
background-color: #1c1c1c;
|
|
margin-top: 0;
|
|
margin-bottom: 0;
|
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
|
font-size: 12px;
|
|
padding: 16px;
|
|
overflow: auto;
|
|
line-height: 1.45;
|
|
border-radius: 3px;
|
|
white-space: pre-wrap;
|
|
overflow-wrap: break-word;
|
|
color: #DDD;
|
|
}
|
|
|
|
.inlinecode {
|
|
box-sizing: border-box;
|
|
padding: 0.2em 0.4em;
|
|
margin: 0;
|
|
font-size: 85%;
|
|
background-color: rgba(27,31,35,0.05);
|
|
border-radius: 3px;
|
|
font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace;
|
|
}
|
|
|
|
.log.bold {
|
|
font-weight: bold;
|
|
}
|
|
|
|
.log .v {
|
|
color: #888888;
|
|
}
|
|
|
|
.log .d {
|
|
color: #00DDDD;
|
|
}
|
|
|
|
.log .c {
|
|
color: magenta;
|
|
}
|
|
|
|
.log .i {
|
|
color: limegreen;
|
|
}
|
|
|
|
.log .w {
|
|
color: yellow;
|
|
}
|
|
|
|
.log .e {
|
|
color: red;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.log .e {
|
|
color: red;
|
|
}
|
|
|
|
.log .ww {
|
|
color: white;
|
|
}
|
|
|
|
.modal {
|
|
width: 95%;
|
|
max-height: 90%;
|
|
height: 85% !important;
|
|
}
|
|
|
|
.page-footer {
|
|
padding-top: 0;
|
|
}
|
|
|
|
body {
|
|
display: flex;
|
|
min-height: 100vh;
|
|
flex-direction: column;
|
|
}
|
|
|
|
main {
|
|
flex: 1 0 auto;
|
|
}
|
|
|
|
ul.browser-default {
|
|
padding-left: 30px;
|
|
margin-top: 10px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
ul.browser-default li {
|
|
list-style-type: initial;
|
|
}
|
|
|
|
ul.stepper:not(.horizontal) .step.active::before, ul.stepper:not(.horizontal) .step.done::before, ul.stepper.horizontal .step.active .step-title::before, ul.stepper.horizontal .step.done .step-title::before {
|
|
background-color: #3f51b5 !important;
|
|
}
|
|
|
|
.select-port-container {
|
|
margin-top: 8px;
|
|
margin-right: 24px;
|
|
width: 350px;
|
|
}
|
|
|
|
.dropdown-trigger {
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* https://github.com/tnhu/status-indicator/blob/master/styles.css */
|
|
.status-indicator .status-indicator-icon {
|
|
display: inline-block;
|
|
border-radius: 50%;
|
|
width: 10px;
|
|
height: 10px;
|
|
}
|
|
|
|
.status-indicator.unknown .status-indicator-icon {
|
|
background-color: rgb(216, 226, 233);
|
|
}
|
|
|
|
.status-indicator.unknown .status-indicator-text::after {
|
|
content: "Unknown status";
|
|
}
|
|
|
|
.status-indicator.offline .status-indicator-icon {
|
|
background-color: rgb(255, 77, 77);
|
|
}
|
|
|
|
.status-indicator.offline .status-indicator-text::after {
|
|
content: "Offline";
|
|
}
|
|
|
|
.status-indicator.not-responding .status-indicator-icon {
|
|
background-color: rgb(255, 170, 0);
|
|
}
|
|
|
|
.status-indicator.not-responding .status-indicator-text::after {
|
|
content: "Not Responding";
|
|
}
|
|
|
|
@keyframes status-indicator-pulse-online {
|
|
0% {
|
|
box-shadow: 0 0 0 0 rgba(75, 210, 143, .5);
|
|
}
|
|
25% {
|
|
box-shadow: 0 0 0 10px rgba(75, 210, 143, 0);
|
|
}
|
|
30% {
|
|
box-shadow: 0 0 0 0 rgba(75, 210, 143, 0);
|
|
}
|
|
}
|
|
|
|
.status-indicator.online .status-indicator-icon {
|
|
background-color: rgb(75, 210, 143);
|
|
animation-duration: 5s;
|
|
animation-timing-function: ease-in-out;
|
|
animation-iteration-count: infinite;
|
|
animation-direction: normal;
|
|
animation-delay: 0s;
|
|
animation-fill-mode: none;
|
|
animation-name: status-indicator-pulse-online;
|
|
}
|
|
|
|
.status-indicator.online .status-indicator-text::after {
|
|
content: "Online";
|
|
}
|
|
</style>
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<nav>
|
|
<div class="nav-wrapper indigo">
|
|
<a href="#" class="brand-logo left">esphomeyaml Dashboard</a>
|
|
<div class="select-port-container right" id="select-port-target">
|
|
<select></select>
|
|
</div>
|
|
</div>
|
|
</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>
|
|
</header>
|
|
|
|
<main>
|
|
<div class="container">
|
|
{% for i, entry in enumerate(entries) %}
|
|
<div class="row">
|
|
<div class="col s8 offset-s2 m10 offset-m1 l12">
|
|
<div class="card horizontal">
|
|
<div class="card-image center-align">
|
|
<i class="material-icons very-large icon-grey">memory</i>
|
|
</div>
|
|
<div class="card-stacked">
|
|
<div class="card-content">
|
|
<span class="card-title">
|
|
{{ escape(entry.name) }}
|
|
<i class="material-icons right dropdown-trigger" data-target="dropdown-{{ i }}">more_vert</i>
|
|
</span>
|
|
<p>
|
|
<span class="status-indicator unknown" data-node="{{ entry.filename }}">
|
|
<span class="status-indicator-icon"></span>
|
|
<span class="status-indicator-text"></span></span>. Full path: <code class="inlinecode">{{ escape(entry.full_path) }}</code>
|
|
</p>
|
|
</div>
|
|
<div class="card-action">
|
|
<a href="#" class="action-upload" data-node="{{ entry.filename }}">Upload</a>
|
|
<a href="#" class="action-compile" data-node="{{ entry.filename }}">Compile</a>
|
|
<a href="#" class="action-show-logs" data-node="{{ entry.filename }}">Show Logs</a>
|
|
<a href="#" class="action-validate" data-node="{{ entry.filename }}">Validate</a>
|
|
</div>
|
|
<ul id="dropdown-{{ i }}" class="dropdown-content">
|
|
<li><a href="#" class="action-clean-mqtt" data-node="{{ entry.filename }}">Clean MQTT</a></li>
|
|
<li><a href="#" class="action-clean" data-node="{{ entry.filename }}">Clean Build</a></li>
|
|
<li><a href="#" class="action-hass-config" data-node="{{ entry.filename }}">Home Assistant Configuration</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% end %}
|
|
</div>
|
|
|
|
<div id="modal-logs" class="modal modal-fixed-footer">
|
|
<div class="modal-content">
|
|
<h4>Show Logs <code class="inlinecode filename"></code></h4>
|
|
<div class="log-container">
|
|
<pre class="log"></pre>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<a class="modal-close waves-effect waves-green btn-flat stop-logs">Close</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="modal-upload" class="modal modal-fixed-footer">
|
|
<div class="modal-content">
|
|
<h4>Compile And Upload <code class="inlinecode filename"></code></h4>
|
|
<div class="log-container">
|
|
<pre class="log"></pre>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<a class="modal-close waves-effect waves-green btn-flat stop-logs">Stop</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="modal-compile" class="modal modal-fixed-footer">
|
|
<div class="modal-content">
|
|
<h4>Compile <code class="inlinecode filename"></code></h4>
|
|
<div class="log-container">
|
|
<pre class="log"></pre>
|
|
</div>
|
|
</div>
|
|
<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 stop-logs">Stop</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="modal-validate" class="modal modal-fixed-footer">
|
|
<div class="modal-content">
|
|
<h4>Validate <code class="inlinecode filename"></code></h4>
|
|
<div class="log-container">
|
|
<pre class="log"></pre>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<a class="modal-close waves-effect waves-green btn-flat stop-logs">Stop</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="modal-wizard" class="modal">
|
|
<div class="modal-content">
|
|
<form action="/wizard.html" method="POST">
|
|
<ul class="stepper linear">
|
|
<li class="step active">
|
|
<div class="step-title waves-effect">Introduction And Name</div>
|
|
<div class="step-content">
|
|
<div class="row">
|
|
<p>
|
|
Hi there! I'm the esphomeyaml setup wizard and will guide you through setting up
|
|
your first ESP8266 or ESP32-powered device using esphomeyaml.
|
|
</p>
|
|
<a href="https://www.espressif.com/en/products/hardware/esp8266ex/overview" target="_blank">ESP8266s</a> and
|
|
their successors (the <a href="https://www.espressif.com/en/products/hardware/esp32/overview" target="_blank">ESP32s</a>)
|
|
are great low-cost microcontrollers that can communicate with the outside world using WiFi.
|
|
They're found in many devices such as the popular Sonoff/iTead, but also exist as development boards
|
|
such as the <a href="https://esphomelib.com/esphomeyaml/devices/nodemcu_esp8266.html" target="_blank">NodeMCU</a>.
|
|
<p>
|
|
</p>
|
|
<a href="https://esphomelib.com/esphomeyaml/index.html" target="_blank">esphomeyaml</a>,
|
|
the tool you're using here, creates custom firmwares for these devices using YAML configuration
|
|
files (similar to the ones you might be used to with Home Assistant).
|
|
<p>
|
|
</p>
|
|
This wizard will create a basic YAML configuration file for your "node" (the microcontroller).
|
|
Later, you will be able to customize this file and add some of
|
|
<a href="https://github.com/OttoWinter/esphomelib" target="_blank">esphomelib's</a>
|
|
many integrations.
|
|
<p>
|
|
<p>
|
|
First, I need to know what this node should be called. Choose this name wisely, changing this
|
|
later makes Over-The-Air Update attempts difficult.
|
|
Names must be <strong>lowercase</strong> and <strong>must not contain spaces</strong> (allowed characters: <code class="inlinecode">a-z</code>,
|
|
<code class="inlinecode">0-9</code> and <code class="inlinecode">_</code>)
|
|
</p>
|
|
<div class="input-field col s12">
|
|
<input id="node_name" class="validate" type="text" name="name" required>
|
|
<label for="node_name">Name of node</label>
|
|
</div>
|
|
</div>
|
|
<div class="step-actions">
|
|
<button class="waves-effect waves-dark btn indigo next-step">CONTINUE</button>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
<li class="step">
|
|
<div class="step-title waves-effect">Device Type</div>
|
|
<div class="step-content">
|
|
<div class="row">
|
|
<p>
|
|
Great! Now I need to know what type of microcontroller you're using so that I can compile firmware for them.
|
|
Please choose either ESP32 or ESP8266 (use ESP8266 for Sonoff devices).
|
|
</p>
|
|
<div class="input-field col s12">
|
|
<select id="esp_type" name="platform" required>
|
|
<option value="ESP8266">ESP8266</option>
|
|
<option value="ESP32">ESP32</option>
|
|
</select>
|
|
<label>Microcontroller Type</label>
|
|
</div>
|
|
<p>
|
|
I'm also going to need to know which type of board you're using. Please go to
|
|
<a href="http://docs.platformio.org/en/latest/platforms/espressif32.html#boards" target="_blank">ESP32 boards</a> or
|
|
<a href="http://docs.platformio.org/en/latest/platforms/espressif8266.html#boards" target="_blank">ESP8266 boards</a>,
|
|
find your board and enter it here. For example, enter <code class="inlinecode">nodemcuv2</code>
|
|
for ESP8266 NodeMCU boards. Note: Use <code class="inlinecode">esp01_1m</code> for Sonoff devices.
|
|
</p>
|
|
<div class="input-field col s12">
|
|
<input id="board_type" class="validate" type="text" name="board" required>
|
|
<label for="board_type">Board Type</label>
|
|
</div>
|
|
</div>
|
|
<div class="step-actions">
|
|
<button class="waves-effect waves-dark btn indigo next-step">CONTINUE</button>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
<li class="step">
|
|
<div class="step-title waves-effect">WiFi And Over-The-Air Updates</div>
|
|
<div class="step-content">
|
|
<div class="row">
|
|
<p>
|
|
Thanks! Now I need to know what WiFi Access Point I should instruct the node to connect to.
|
|
Please enter an SSID (name of the WiFi network) and password (leave empty for no password).
|
|
</p>
|
|
<div class="input-field col s12">
|
|
<input id="wifi_ssid" class="validate" type="text" name="ssid" required>
|
|
<label for="wifi_ssid">WiFi SSID</label>
|
|
</div>
|
|
<div class="input-field col s12">
|
|
<input id="wifi_password" name="psk" type="password">
|
|
<label for="wifi_password">WiFi Password</label>
|
|
</div>
|
|
<p>
|
|
Esphomelib automatically sets up an Over-The-Air update server on the node
|
|
so that you only need to flash a firmware via USB once.
|
|
Optionally, you can set a password for this upload process here:
|
|
</p>
|
|
<div class="input-field col s12">
|
|
<input id="ota_password" class="validate" name="ota_password" type="password">
|
|
<label for="ota_password">OTA Password</label>
|
|
</div>
|
|
</div>
|
|
<div class="step-actions">
|
|
<button class="waves-effect waves-dark btn indigo next-step">CONTINUE</button>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
<li class="step">
|
|
<div class="step-title waves-effect">MQTT</div>
|
|
<div class="step-content">
|
|
<div class="row">
|
|
{% if mqtt_config is None %}
|
|
<p>
|
|
esphomelib connects to your Home Assistant instance via
|
|
<a href="https://www.home-assistant.io/docs/mqtt/">MQTT</a>.
|
|
If you haven't already, please set up
|
|
MQTT on your Home Assistant server, for example with the
|
|
<a href="https://www.home-assistant.io/addons/mosquitto/">Mosquitto Hass.io Add-on</a>.
|
|
</p>
|
|
<p>
|
|
When you're done with that, please enter your MQTT broker here. For example
|
|
<code class="inlinecode">192.168.1.100</code>.
|
|
Please also specify the MQTT username and password you wish esphomelib to use
|
|
(leave them empty if you're not using any authentication).
|
|
</p>
|
|
{% else %}
|
|
<p>
|
|
It looks like you've already set up MQTT, the values below are taken from your "{{ escape(mqtt_config['addon']) }}" add-on.
|
|
</p>
|
|
{% end %}
|
|
<div class="input-field col s12">
|
|
{% if mqtt_config is None %}
|
|
<input id="mqtt_broker" class="validate" type="text" name="broker" required>
|
|
{% else %}
|
|
<input id="mqtt_broker" class="validate" type="text" name="broker" value="{{ mqtt_config['host'] }}" required>
|
|
{% end %}
|
|
<label for="mqtt_broker">MQTT Broker</label>
|
|
</div>
|
|
<div class="input-field col s6">
|
|
{% if mqtt_config is None %}
|
|
<input id="mqtt_username" class="validate" type="text" name="mqtt_username">
|
|
{% else %}
|
|
<input id="mqtt_username" class="validate" type="text" name="mqtt_username" value="{{ mqtt_config['username'] }}">
|
|
{% end%}
|
|
<label for="mqtt_username">MQTT Username</label>
|
|
</div>
|
|
<div class="input-field col s6">
|
|
{% if mqtt_config is None %}
|
|
<input id="mqtt_password" class="validate" name="mqtt_password" type="password">
|
|
{% else %}
|
|
<input id="mqtt_password" class="validate" name="mqtt_password" type="password" value="{{ mqtt_config['password'] }}">
|
|
{% end %}
|
|
<label for="mqtt_password">MQTT Password</label>
|
|
</div>
|
|
</div>
|
|
<div class="step-actions">
|
|
<button class="waves-effect waves-dark btn indigo next-step">CONTINUE</button>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
<li class="step">
|
|
<div class="step-title waves-effect">Done!</div>
|
|
<div class="step-content">
|
|
<p>
|
|
Hooray! 🎉🎉🎉 You've successfully created your first esphomeyaml configuration file.
|
|
When you click Submit, I will save this configuration file under
|
|
<code class="inlinecode"><HASS_CONFIG_FOLDER>/esphomeyaml/<NAME_OF_NODE>.yaml</code> and
|
|
you will be able to edit this file with the
|
|
<a href="https://www.home-assistant.io/addons/configurator/" target="_blank">HASS Configuratior add-on</a>.
|
|
</p>
|
|
<h5>Next steps</h5>
|
|
<ul class="browser-default">
|
|
<li>
|
|
Flash the firmware. This can be done using the “UPLOAD” option in the dashboard. See
|
|
<a href="https://esphomelib.com/esphomeyaml/index.html#devices" target="_blank">this</a>
|
|
for guides on how to flash different types of devices. Note that you need to restart this add-on
|
|
for newly plugged in serial devices to be detected.
|
|
</li>
|
|
<li>
|
|
With the current configuration, your node will only connect to WiFi and MQTT. To make it actually <i>do</i>
|
|
stuff, follow
|
|
<a href="https://esphomelib.com/esphomeyaml/guides/getting_started_hassio.html#adding-some-basic-features">
|
|
the rest of the getting started guide
|
|
</a>.
|
|
</li>
|
|
<li>
|
|
See the <a href="https://esphomelib.com/esphomeyaml/index.html" target="_blank">esphomeyaml index</a>
|
|
for a list of supported sensors/devices.
|
|
</li>
|
|
<li>
|
|
Join the <a href="https://discord.gg/KhAMKrd" target="_blank">Discord server</a> and say hi! When I
|
|
have time, I would be happy to help with issues and discuss new features.
|
|
</li>
|
|
<li>
|
|
Star <a href="https://github.com/OttoWinter/esphomelib" target="_blank">esphomelib</a> and
|
|
<a href="https://github.com/OttoWinter/esphomeyaml" target="_blank">esphomeyaml</a> on GitHub
|
|
if you find this software awesome and report issues using the bug trackers there.
|
|
</li>
|
|
</ul>
|
|
<div class="step-actions">
|
|
<button class="waves-effect waves-dark btn indigo" type="submit">SUBMIT</button>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<a href="#!" class="modal-close waves-effect waves-green btn-flat">Abort</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="modal-clean-mqtt" class="modal modal-fixed-footer">
|
|
<div class="modal-content">
|
|
<h4>Clean MQTT discovery <code class="inlinecode filename"></code></h4>
|
|
<div class="log-container">
|
|
<pre class="log"></pre>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<a class="modal-close waves-effect waves-green btn-flat stop-logs">Stop</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="modal-clean" class="modal modal-fixed-footer">
|
|
<div class="modal-content">
|
|
<h4>Clean Build Files <code class="inlinecode filename"></code></h4>
|
|
<div class="log-container">
|
|
<pre class="log"></pre>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<a class="modal-close waves-effect waves-green btn-flat stop-logs">Stop</a>
|
|
</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>
|
|
<div class="log-container">
|
|
<pre class="log"></pre>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<a class="modal-close waves-effect waves-green btn-flat stop-logs">Stop</a>
|
|
</div>
|
|
</div>
|
|
|
|
<a class="btn-floating btn-large ribbon-fab waves-effect waves-light pink accent-2" id="setup-wizard-start">
|
|
<i class="material-icons">add</i>
|
|
</a>
|
|
|
|
<div class="tap-target pink lighten-1 setup-wizard" data-target="setup-wizard-start">
|
|
<div class="tap-target-content">
|
|
<h5>Set up your first Node</h5>
|
|
<p>
|
|
Huh... It seems like you you don't have any esphomeyaml configuration files yet...
|
|
Fortunately, there's a setup wizard that will step you through setting up your first node 🎉
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<footer class="page-footer indigo darken-1">
|
|
<div class="container">
|
|
|
|
</div>
|
|
<div class="footer-copyright">
|
|
<div class="container">
|
|
© 2018 Copyright Otto Winter, Made with <a class="grey-text text-lighten-4" href="https://materializecss.com/" target="_blank">Materialize</a>
|
|
<a class="grey-text text-lighten-4 right" href="{{ docs_link }}" target="_blank">esphomeyaml {{ version }} Documentation</a>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
M.AutoInit(document.body);
|
|
});
|
|
|
|
const colorReplace = (input) => {
|
|
input = input.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
input = input.replace(/\\033\[(?:0;)?31m/g, '<span class="e">');
|
|
input = input.replace(/\\033\[(?:1;)?31m/g, '<span class="e bold">');
|
|
input = input.replace(/\\033\[(?:0;)?32m/g, '<span class="i">');
|
|
input = input.replace(/\\033\[(?:1;)?32m/g, '<span class="i bold">');
|
|
input = input.replace(/\\033\[(?:0;)?33m/g, '<span class="w">');
|
|
input = input.replace(/\\033\[(?:1;)?33m/g, '<span class="w bold">');
|
|
input = input.replace(/\\033\[(?:0;)?35m/g, '<span class="c">');
|
|
input = input.replace(/\\033\[(?:1;)?35m/g, '<span class="c bold">');
|
|
input = input.replace(/\\033\[(?:0;)?36m/g, '<span class="d">');
|
|
input = input.replace(/\\033\[(?:1;)?36m/g, '<span class="d bold">');
|
|
input = input.replace(/\\033\[(?:0;)?37m/g, '<span class="v">');
|
|
input = input.replace(/\\033\[(?:1;)?37m/g, '<span class="v bold">');
|
|
input = input.replace(/\\033\[(?:0;)?38m/g, '<span class="vv">');
|
|
input = input.replace(/\\033\[(?:1;)?38m/g, '<span class="vv bold">');
|
|
input = input.replace(/\\033\[0m/g, '</span>');
|
|
|
|
return input;
|
|
};
|
|
|
|
let configuration = "";
|
|
let wsProtocol = "ws:";
|
|
if (window.location.protocol === "https:") {
|
|
wsProtocol = 'wss:';
|
|
}
|
|
const wsUrl = wsProtocol + '//' + window.location.hostname + ':' + window.location.port;
|
|
|
|
let isFetchingPing = false;
|
|
const fetchPing = () => {
|
|
if (isFetchingPing)
|
|
return;
|
|
isFetchingPing = true;
|
|
|
|
fetch('/ping', {credentials: "same-origin"}).then(res => res.json())
|
|
.then(response => {
|
|
for (let filename in response) {
|
|
let node = document.querySelector(`.status-indicator[data-node="${filename}"]`);
|
|
if (node === null)
|
|
continue;
|
|
|
|
let status = response[filename];
|
|
let klass;
|
|
if (status === null) {
|
|
klass = 'unknown';
|
|
} else if (status === true) {
|
|
klass = 'online';
|
|
node.setAttribute('data-last-connected', Date.now().toString());
|
|
} else if (node.hasAttribute('data-last-connected')) {
|
|
const attr = parseInt(node.getAttribute('data-last-connected'));
|
|
if (Date.now() - attr <= 5000) {
|
|
klass = 'not-responding';
|
|
} else {
|
|
klass = 'offline';
|
|
}
|
|
} else {
|
|
klass = 'offline';
|
|
}
|
|
|
|
if (node.classList.contains(klass))
|
|
continue;
|
|
|
|
node.classList.remove('unknown', 'online', 'offline', 'not-responding');
|
|
node.classList.add(klass);
|
|
}
|
|
|
|
isFetchingPing = false;
|
|
});
|
|
};
|
|
setInterval(fetchPing, 2000);
|
|
fetchPing();
|
|
|
|
const portSelect = document.querySelector('.nav-wrapper select');
|
|
let ports = [];
|
|
|
|
const fetchSerialPorts = (begin=false) => {
|
|
fetch('/serial-ports', {credentials: "same-origin"}).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;
|
|
}
|
|
const hasNewPort = response.length >= ports.length;
|
|
|
|
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 && hasNewPort)
|
|
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, 5000);
|
|
fetchSerialPorts(true);
|
|
|
|
const logsModalElem = document.getElementById("modal-logs");
|
|
|
|
document.querySelectorAll(".action-show-logs").forEach((showLogs) => {
|
|
showLogs.addEventListener('click', (e) => {
|
|
configuration = e.target.getAttribute('data-node');
|
|
const modalInstance = M.Modal.getInstance(logsModalElem);
|
|
const log = logsModalElem.querySelector(".log");
|
|
log.innerHTML = "";
|
|
const stopLogsButton = logsModalElem.querySelector(".stop-logs");
|
|
let stopped = false;
|
|
stopLogsButton.innerHTML = "Stop";
|
|
modalInstance.open();
|
|
|
|
const filenameField = logsModalElem.querySelector('.filename');
|
|
filenameField.innerHTML = configuration;
|
|
|
|
const logSocket = new WebSocket(wsUrl + "/logs");
|
|
logSocket.addEventListener('message', (event) => {
|
|
const data = JSON.parse(event.data);
|
|
if (data.event === "line") {
|
|
const msg = data.data;
|
|
log.innerHTML += colorReplace(msg);
|
|
} else if (data.event === "exit") {
|
|
if (data.code === 0) {
|
|
M.toast({html: "Program exited successfully."});
|
|
} else {
|
|
M.toast({html: `Program failed with code ${data.code}`});
|
|
}
|
|
|
|
stopLogsButton.innerHTML = "Close";
|
|
stopped = true;
|
|
}
|
|
});
|
|
logSocket.addEventListener('open', () => {
|
|
const msg = JSON.stringify({configuration: configuration, port: getUploadPort()});
|
|
logSocket.send(msg);
|
|
});
|
|
logSocket.addEventListener('close', () => {
|
|
if (!stopped) {
|
|
M.toast({html: 'Terminated process.'});
|
|
}
|
|
});
|
|
modalInstance.options.onCloseStart = () => {
|
|
logSocket.close();
|
|
};
|
|
});
|
|
});
|
|
|
|
const uploadModalElem = document.getElementById("modal-upload");
|
|
|
|
document.querySelectorAll(".action-upload").forEach((upload) => {
|
|
upload.addEventListener('click', (e) => {
|
|
configuration = e.target.getAttribute('data-node');
|
|
const modalInstance = M.Modal.getInstance(uploadModalElem);
|
|
const log = uploadModalElem.querySelector(".log");
|
|
log.innerHTML = "";
|
|
const stopLogsButton = uploadModalElem.querySelector(".stop-logs");
|
|
let stopped = false;
|
|
stopLogsButton.innerHTML = "Stop";
|
|
modalInstance.open();
|
|
|
|
const filenameField = uploadModalElem.querySelector('.filename');
|
|
filenameField.innerHTML = configuration;
|
|
|
|
const logSocket = new WebSocket(wsUrl + "/run");
|
|
logSocket.addEventListener('message', (event) => {
|
|
const data = JSON.parse(event.data);
|
|
if (data.event === "line") {
|
|
const msg = data.data;
|
|
log.innerHTML += colorReplace(msg);
|
|
} else if (data.event === "exit") {
|
|
if (data.code === 0) {
|
|
M.toast({html: "Program exited successfully."});
|
|
} else {
|
|
M.toast({html: `Program failed with code ${data.code}`});
|
|
}
|
|
|
|
stopLogsButton.innerHTML = "Close";
|
|
stopped = true;
|
|
}
|
|
});
|
|
logSocket.addEventListener('open', () => {
|
|
const msg = JSON.stringify({configuration: configuration, port: getUploadPort()});
|
|
logSocket.send(msg);
|
|
});
|
|
logSocket.addEventListener('close', () => {
|
|
if (!stopped) {
|
|
M.toast({html: 'Terminated process.'});
|
|
}
|
|
});
|
|
modalInstance.options.onCloseStart = () => {
|
|
logSocket.close();
|
|
};
|
|
});
|
|
});
|
|
|
|
const validateModalElem = document.getElementById("modal-validate");
|
|
|
|
document.querySelectorAll(".action-validate").forEach((upload) => {
|
|
upload.addEventListener('click', (e) => {
|
|
configuration = e.target.getAttribute('data-node');
|
|
const modalInstance = M.Modal.getInstance(validateModalElem);
|
|
const log = validateModalElem.querySelector(".log");
|
|
log.innerHTML = "";
|
|
const stopLogsButton = validateModalElem.querySelector(".stop-logs");
|
|
let stopped = false;
|
|
stopLogsButton.innerHTML = "Stop";
|
|
modalInstance.open();
|
|
|
|
const filenameField = validateModalElem.querySelector('.filename');
|
|
filenameField.innerHTML = configuration;
|
|
|
|
const logSocket = new WebSocket(wsUrl + "/validate");
|
|
logSocket.addEventListener('message', (event) => {
|
|
const data = JSON.parse(event.data);
|
|
if (data.event === "line") {
|
|
const msg = data.data;
|
|
log.innerHTML += colorReplace(msg);
|
|
} else if (data.event === "exit") {
|
|
if (data.code === 0) {
|
|
M.toast({
|
|
html: `<code class="inlinecode">${configuration}</code> is valid 👍`,
|
|
displayLength: 5000,
|
|
});
|
|
} else {
|
|
M.toast({
|
|
html: `<code class="inlinecode">${configuration}</code> is invalid 😕`,
|
|
displayLength: 5000,
|
|
});
|
|
}
|
|
|
|
stopLogsButton.innerHTML = "Close";
|
|
stopped = true;
|
|
}
|
|
});
|
|
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();
|
|
};
|
|
});
|
|
});
|
|
|
|
const compileModalElem = document.getElementById("modal-compile");
|
|
const downloadButton = compileModalElem.querySelector('.download-binary');
|
|
|
|
document.querySelectorAll(".action-compile").forEach((upload) => {
|
|
upload.addEventListener('click', (e) => {
|
|
configuration = e.target.getAttribute('data-node');
|
|
const modalInstance = M.Modal.getInstance(compileModalElem);
|
|
const log = compileModalElem.querySelector(".log");
|
|
log.innerHTML = "";
|
|
const stopLogsButton = compileModalElem.querySelector(".stop-logs");
|
|
let stopped = false;
|
|
stopLogsButton.innerHTML = "Stop";
|
|
downloadButton.classList.add('disabled');
|
|
|
|
modalInstance.open();
|
|
|
|
const filenameField = compileModalElem.querySelector('.filename');
|
|
filenameField.innerHTML = configuration;
|
|
|
|
const logSocket = new WebSocket(wsUrl + "/compile");
|
|
logSocket.addEventListener('message', (event) => {
|
|
const data = JSON.parse(event.data);
|
|
if (data.event === "line") {
|
|
const msg = data.data;
|
|
log.innerHTML += colorReplace(msg);
|
|
} 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;
|
|
}
|
|
});
|
|
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();
|
|
};
|
|
});
|
|
});
|
|
downloadButton.addEventListener('click', () => {
|
|
const link = document.createElement("a");
|
|
link.download = name;
|
|
link.href = '/download.bin?configuration=' + encodeURIComponent(configuration);
|
|
link.click();
|
|
});
|
|
|
|
const cleanMqttModalElem = document.getElementById("modal-clean-mqtt");
|
|
|
|
document.querySelectorAll(".action-clean-mqtt").forEach((btn) => {
|
|
btn.addEventListener('click', (e) => {
|
|
configuration = e.target.getAttribute('data-node');
|
|
const modalInstance = M.Modal.getInstance(cleanMqttModalElem);
|
|
const log = cleanMqttModalElem.querySelector(".log");
|
|
log.innerHTML = "";
|
|
const stopLogsButton = cleanMqttModalElem.querySelector(".stop-logs");
|
|
let stopped = false;
|
|
stopLogsButton.innerHTML = "Stop";
|
|
modalInstance.open();
|
|
|
|
const filenameField = cleanMqttModalElem.querySelector('.filename');
|
|
filenameField.innerHTML = configuration;
|
|
|
|
const logSocket = new WebSocket(wsUrl + "/clean-mqtt");
|
|
logSocket.addEventListener('message', (event) => {
|
|
const data = JSON.parse(event.data);
|
|
if (data.event === "line") {
|
|
const msg = data.data;
|
|
log.innerHTML += colorReplace(msg);
|
|
} else if (data.event === "exit") {
|
|
stopLogsButton.innerHTML = "Close";
|
|
stopped = true;
|
|
}
|
|
});
|
|
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();
|
|
};
|
|
});
|
|
});
|
|
|
|
const cleanModalElem = document.getElementById("modal-clean");
|
|
|
|
document.querySelectorAll(".action-clean").forEach((btn) => {
|
|
btn.addEventListener('click', (e) => {
|
|
configuration = e.target.getAttribute('data-node');
|
|
const modalInstance = M.Modal.getInstance(cleanModalElem);
|
|
const log = cleanModalElem.querySelector(".log");
|
|
log.innerHTML = "";
|
|
const stopLogsButton = cleanModalElem.querySelector(".stop-logs");
|
|
let stopped = false;
|
|
stopLogsButton.innerHTML = "Stop";
|
|
modalInstance.open();
|
|
|
|
const filenameField = cleanModalElem.querySelector('.filename');
|
|
filenameField.innerHTML = configuration;
|
|
|
|
const logSocket = new WebSocket(wsUrl + "/clean");
|
|
logSocket.addEventListener('message', (event) => {
|
|
const data = JSON.parse(event.data);
|
|
if (data.event === "line") {
|
|
const msg = data.data;
|
|
log.innerHTML += colorReplace(msg);
|
|
} 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;
|
|
}
|
|
});
|
|
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();
|
|
};
|
|
});
|
|
});
|
|
|
|
const hassConfigModalElem = document.getElementById("modal-hass-config");
|
|
|
|
document.querySelectorAll(".action-hass-config").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 stopLogsButton = hassConfigModalElem.querySelector(".stop-logs");
|
|
let stopped = false;
|
|
stopLogsButton.innerHTML = "Stop";
|
|
modalInstance.open();
|
|
|
|
const filenameField = hassConfigModalElem.querySelector('.filename');
|
|
filenameField.innerHTML = configuration;
|
|
|
|
const logSocket = new WebSocket(wsUrl + "/hass-config");
|
|
logSocket.addEventListener('message', (event) => {
|
|
const data = JSON.parse(event.data);
|
|
if (data.event === "line") {
|
|
const msg = data.data;
|
|
log.innerHTML += colorReplace(msg);
|
|
} 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;
|
|
}
|
|
});
|
|
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();
|
|
};
|
|
});
|
|
});
|
|
|
|
const modalSetupElem = document.getElementById("modal-wizard");
|
|
const setupWizardStart = document.getElementById('setup-wizard-start');
|
|
const startWizard = () => {
|
|
const modalInstance = M.Modal.getInstance(modalSetupElem);
|
|
modalInstance.open();
|
|
|
|
modalInstance.options.onCloseStart = () => {
|
|
|
|
};
|
|
|
|
$('.stepper').activateStepper({
|
|
linearStepsNavigation: false,
|
|
autoFocusInput: true,
|
|
autoFormCreation: true,
|
|
showFeedbackLoader: true,
|
|
parallel: false
|
|
});
|
|
};
|
|
setupWizardStart.addEventListener('click', startWizard);
|
|
</script>
|
|
|
|
{% if len(entries) == 0 %}
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const tapTargetElem = document.querySelector('.tap-target.setup-wizard');
|
|
const tapTargetInstance = M.TapTarget.getInstance(tapTargetElem);
|
|
tapTargetInstance.options.onOpen = () => {
|
|
$('.tap-target-origin').on('click', () => {
|
|
startWizard();
|
|
});
|
|
};
|
|
tapTargetInstance.open();
|
|
});
|
|
</script>
|
|
{% 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>
|
|
</html>
|