HassIO add-on (#18)

* HassIO Beginnings

* Updates

* Fix pylint errors

* Fix pylint error
This commit is contained in:
Otto Winter 2018-05-21 16:40:22 +02:00 committed by GitHub
parent ebb6d0d464
commit 94d7ac4ef0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1140 additions and 106 deletions

4
MANIFEST.in Normal file
View File

@ -0,0 +1,4 @@
include README.md
include esphomeyaml/hassio/templates/index.html
include esphomeyaml/hassio/static/materialize-stepper.min.css
include esphomeyaml/hassio/static/materialize-stepper.min.js

View File

@ -1,18 +1,19 @@
from __future__ import print_function
import argparse
from datetime import datetime
import logging
import os
import random
import sys
from esphomeyaml import core, mqtt, wizard, writer, yaml_util, const
from esphomeyaml import const, core, mqtt, wizard, writer, yaml_util
from esphomeyaml.config import core_to_code, get_component, iter_components, read_config
from esphomeyaml.const import CONF_BAUD_RATE, CONF_DOMAIN, CONF_ESPHOMEYAML, CONF_HOSTNAME, \
CONF_LOGGER, CONF_MANUAL_IP, CONF_NAME, CONF_STATIC_IP, CONF_WIFI
CONF_LOGGER, CONF_MANUAL_IP, CONF_NAME, CONF_STATIC_IP, CONF_WIFI, ESP_PLATFORM_ESP8266
from esphomeyaml.core import ESPHomeYAMLError
from esphomeyaml.helpers import AssignmentExpression, RawStatement, _EXPRESSIONS, add, add_task, \
color, get_variable, indent, quote, statement, Expression
from esphomeyaml.helpers import AssignmentExpression, Expression, RawStatement, _EXPRESSIONS, add, \
add_task, color, get_variable, indent, quote, statement
_LOGGER = logging.getLogger(__name__)
@ -27,28 +28,27 @@ def get_base_path(config):
return os.path.join(os.path.dirname(core.CONFIG_PATH), get_name(config))
def discover_serial_ports():
def get_serial_ports():
# from https://github.com/pyserial/pyserial/blob/master/serial/tools/list_ports.py
try:
from serial.tools.list_ports import comports
except ImportError:
return None
result = []
descs = []
for port, desc, info in comports():
if not port:
continue
if "VID:PID" in info:
result.append(port)
descs.append(desc)
result.append((port, desc))
return result
def choose_serial_port(config):
result = get_serial_ports()
if not result:
return None
return 'OTA'
print(u"Found multiple serial port options, please choose one:")
for i, (res, desc) in enumerate(zip(result, descs)):
for i, (res, desc) in enumerate(result):
print(u" [{}] {} ({})".format(i, res, desc))
print(u" [{}] Over The Air".format(len(result)))
print(u" [{}] Over The Air ({})".format(len(result), get_upload_host(config)))
print()
while True:
opt = raw_input('(number): ')
@ -63,11 +63,11 @@ def discover_serial_ports():
except ValueError:
print(color('red', u"Invalid option: '{}'".format(opt)))
if opt == len(result):
return None
return result[opt]
return 'OTA'
return result[opt][0]
def run_platformio(*cmd):
def run_platformio(*cmd, **kwargs):
def mock_exit(return_code):
raise SystemExit(return_code)
@ -76,10 +76,13 @@ def run_platformio(*cmd):
full_cmd = u' '.join(quote(x) for x in cmd)
_LOGGER.info(u"Running: %s", full_cmd)
try:
func = kwargs.get('main')
if func is None:
import platformio.__main__
func = platformio.__main__.main
sys.argv = list(cmd)
sys.exit = mock_exit
return platformio.__main__.main()
return func() or 0
except KeyboardInterrupt:
return 1
except SystemExit as err:
@ -92,13 +95,19 @@ def run_platformio(*cmd):
sys.exit = orig_exit
def run_miniterm(config, port):
from serial.tools import miniterm
def run_miniterm(config, port, escape=False):
import serial
baud_rate = config.get(CONF_LOGGER, {}).get(CONF_BAUD_RATE, 115200)
sys.argv = ['miniterm', '--raw', '--exit-char', '3']
miniterm.main(
default_port=port,
default_baudrate=baud_rate)
_LOGGER.info("Starting log output from %s with baud rate %s", port, baud_rate)
with serial.Serial(port, baudrate=baud_rate) as ser:
while True:
line = ser.readline()
time = datetime.now().time().strftime('[%H:%M:%S]')
message = time + line.decode('unicode-escape').replace('\r', '').replace('\n', '')
if escape:
message = message.replace('\033', '\\033').encode('ascii', 'replace')
print(message)
def write_cpp(config):
@ -153,9 +162,32 @@ def compile_program(config):
return run_platformio('platformio', 'run', '-d', get_base_path(config))
def get_upload_host(config):
if CONF_MANUAL_IP in config[CONF_WIFI]:
host = str(config[CONF_WIFI][CONF_MANUAL_IP][CONF_STATIC_IP])
elif CONF_HOSTNAME in config[CONF_WIFI]:
host = config[CONF_WIFI][CONF_HOSTNAME] + config[CONF_WIFI][CONF_DOMAIN]
else:
host = config[CONF_ESPHOMEYAML][CONF_NAME] + config[CONF_WIFI][CONF_DOMAIN]
return host
def upload_using_esptool(config, port):
import esptool
name = get_name(config)
path = os.path.join(get_base_path(config), '.pioenvs', name, 'firmware.bin')
# pylint: disable=protected-access
return run_platformio('esptool.py', '--before', 'default_reset', '--after', 'hard_reset',
'--chip', 'esp8266', '--port', port, 'write_flash', '0x0',
path, main=esptool._main)
def upload_program(config, args, port):
_LOGGER.info("Uploading binary...")
if port is not None:
if port != 'OTA':
if core.ESP_PLATFORM == ESP_PLATFORM_ESP8266 and args.use_esptoolpy:
return upload_using_esptool(config, port)
return run_platformio('platformio', 'run', '-d', get_base_path(config),
'-t', 'upload', '--upload-port', port)
@ -163,12 +195,7 @@ def upload_program(config, args, port):
_LOGGER.error("No serial port found and OTA not enabled. Can't upload!")
return -1
if CONF_MANUAL_IP in config[CONF_WIFI]:
host = str(config[CONF_WIFI][CONF_MANUAL_IP][CONF_STATIC_IP])
elif CONF_HOSTNAME in config[CONF_WIFI]:
host = config[CONF_WIFI][CONF_HOSTNAME] + config[CONF_WIFI][CONF_DOMAIN]
else:
host = config[CONF_ESPHOMEYAML][CONF_NAME] + config[CONF_WIFI][CONF_DOMAIN]
host = get_upload_host(config)
from esphomeyaml.components import ota
from esphomeyaml import espota
@ -184,11 +211,12 @@ def upload_program(config, args, port):
return espota.main(espota_args)
def show_logs(config, args, port):
if port is not None and port != 'OTA':
run_miniterm(config, port)
def show_logs(config, args, port, escape=False):
if port != 'OTA':
run_miniterm(config, port, escape=escape)
return 0
return mqtt.show_logs(config, args.topic, args.username, args.password, args.client_id)
return mqtt.show_logs(config, args.topic, args.username, args.password, args.client_id,
escape=escape)
def clean_mqtt(config, args):
@ -221,11 +249,98 @@ def setup_log():
pass
def main():
setup_log()
def command_wizard(args):
return wizard.wizard(args.configuration)
def command_config(args, config):
print(yaml_util.dump(config))
return 0
def command_compile(args, config):
exit_code = write_cpp(config)
if exit_code != 0:
return exit_code
exit_code = compile_program(config)
if exit_code != 0:
return exit_code
_LOGGER.info(u"Successfully compiled program.")
return 0
def command_upload(args, config):
port = args.upload_port or choose_serial_port(config)
exit_code = upload_program(config, args, port)
if exit_code != 0:
return exit_code
_LOGGER.info(u"Successfully uploaded program.")
return 0
def command_logs(args, config):
port = args.serial_port or choose_serial_port(config)
return show_logs(config, args, port, escape=args.escape)
def command_run(args, config):
exit_code = write_cpp(config)
if exit_code != 0:
return exit_code
exit_code = compile_program(config)
if exit_code != 0:
return exit_code
_LOGGER.info(u"Successfully compiled program.")
port = args.upload_port or choose_serial_port(config)
exit_code = upload_program(config, args, port)
if exit_code != 0:
return exit_code
_LOGGER.info(u"Successfully uploaded program.")
if args.no_logs:
return 0
return show_logs(config, args, port, escape=args.escape)
def command_clean_mqtt(args, config):
return clean_mqtt(config, args)
def command_mqtt_fingerprint(args, config):
return mqtt.get_fingerprint(config)
def command_version(args):
print(u"Version: {}".format(const.__version__))
return 0
def command_hassio(args):
from esphomeyaml.hassio import hassio
return hassio.start_web_server(args)
PRE_CONFIG_ACTIONS = {
'wizard': command_wizard,
'version': command_version,
'hassio': command_hassio
}
POST_CONFIG_ACTIONS = {
'config': command_config,
'compile': command_compile,
'upload': command_upload,
'logs': command_logs,
'run': command_run,
'clean-mqtt': command_clean_mqtt,
'mqtt-fingerprint': command_mqtt_fingerprint,
}
def parse_args(argv):
parser = argparse.ArgumentParser(prog='esphomeyaml')
parser.add_argument('configuration', help='Your YAML configuration file.')
subparsers = parser.add_subparsers(help='Commands', dest='command')
subparsers.required = True
subparsers.add_parser('config', help='Validate the configuration and spit it out.')
@ -237,6 +352,9 @@ def main():
parser_upload.add_argument('--upload-port', help="Manually specify the upload port to use. "
"For example /dev/cu.SLAB_USBtoUART.")
parser_upload.add_argument('--host-port', help="Specify the host port.", type=int)
parser_upload.add_argument('--use-esptoolpy',
help="Use esptool.py for HassIO (only for ESP8266)",
action='store_true')
parser_logs = subparsers.add_parser('logs', help='Validate the configuration '
'and show all MQTT logs.')
@ -246,6 +364,8 @@ def main():
parser_logs.add_argument('--client-id', help='Manually set the client id.')
parser_logs.add_argument('--serial-port', help="Manually specify a serial port to use"
"For example /dev/cu.SLAB_USBtoUART.")
parser_logs.add_argument('--escape', help="Escape ANSI color codes for HassIO",
action='store_true')
parser_run = subparsers.add_parser('run', help='Validate the configuration, create a binary, '
'upload it, and start MQTT logs.')
@ -258,6 +378,10 @@ def main():
parser_run.add_argument('--username', help='Manually set the MQTT username for logs.')
parser_run.add_argument('--password', help='Manually set the MQTT password for logs.')
parser_run.add_argument('--client-id', help='Manually set the client id for logs.')
parser_run.add_argument('--escape', help="Escape ANSI color codes for HassIO",
action='store_true')
parser_run.add_argument('--use-esptoolpy', help="Use esptool.py for HassIO (only for ESP8266)",
action='store_true')
parser_clean = subparsers.add_parser('clean-mqtt', help="Helper to clear an MQTT topic from "
"retain messages.")
@ -270,12 +394,25 @@ def main():
"you through setting up esphomeyaml.")
subparsers.add_parser('mqtt-fingerprint', help="Get the SSL fingerprint from a MQTT broker.")
subparsers.add_parser('version', help="Print the esphomeyaml version and exit.")
args = parser.parse_args()
hassio = subparsers.add_parser('hassio', help="Create a simple webserver for a HassIO add-on.")
hassio.add_argument("--port", help="The HTTP port to open connections on.", type=int,
default=6052)
if args.command == 'wizard':
return wizard.wizard(args.configuration)
return parser.parse_args(argv[1:])
def run_esphomeyaml(argv):
setup_log()
args = parse_args(argv)
if args.command in PRE_CONFIG_ACTIONS:
try:
return PRE_CONFIG_ACTIONS[args.command](args)
except ESPHomeYAMLError as e:
_LOGGER.error(e)
return 1
core.CONFIG_PATH = args.configuration
@ -283,58 +420,25 @@ def main():
if config is None:
return 1
if args.command == 'config':
print(yaml_util.dump(config))
return 0
elif args.command == 'compile':
if args.command in POST_CONFIG_ACTIONS:
try:
exit_code = write_cpp(config)
return POST_CONFIG_ACTIONS[args.command](args, config)
except ESPHomeYAMLError as e:
_LOGGER.error(e)
return 1
if exit_code != 0:
return exit_code
exit_code = compile_program(config)
if exit_code != 0:
return exit_code
_LOGGER.info(u"Successfully compiled program.")
return 0
elif args.command == 'upload':
port = args.upload_port or discover_serial_ports()
exit_code = upload_program(config, args, port)
if exit_code != 0:
return exit_code
_LOGGER.info(u"Successfully uploaded program.")
return 0
elif args.command == 'logs':
port = args.serial_port or discover_serial_ports()
return show_logs(config, args, port)
elif args.command == 'clean-mqtt':
return clean_mqtt(config, args)
elif args.command == 'mqtt-fingerprint':
return mqtt.get_fingerprint(config)
elif args.command == 'run':
exit_code = write_cpp(config)
if exit_code != 0:
return exit_code
exit_code = compile_program(config)
if exit_code != 0:
return exit_code
_LOGGER.info(u"Successfully compiled program.")
port = args.upload_port or discover_serial_ports()
exit_code = upload_program(config, args, port)
if exit_code != 0:
return exit_code
_LOGGER.info(u"Successfully uploaded program.")
if args.no_logs:
return 0
return show_logs(config, args, port)
elif args.command == 'version':
print(u"Version: {}".format(const.__version__))
return 0
print(u"Unknown command {}".format(args.command))
return 1
def main():
try:
return run_esphomeyaml(sys.argv)
except ESPHomeYAMLError as e:
_LOGGER.error(e)
return 1
except KeyboardInterrupt:
return 1
if __name__ == "__main__":
sys.exit(main())

View File

@ -4,11 +4,10 @@ import esphomeyaml.config_validation as cv
from esphomeyaml.components import switch
from esphomeyaml.components.ir_transmitter import IRTransmitterComponent
from esphomeyaml.const import CONF_ADDRESS, CONF_CARRIER_FREQUENCY, CONF_COMMAND, CONF_DATA, \
CONF_ID, CONF_INVERTED, CONF_IR_TRANSMITTER_ID, CONF_LG, CONF_NAME, CONF_NBITS, CONF_NEC, \
CONF_INVERTED, CONF_IR_TRANSMITTER_ID, CONF_LG, CONF_NAME, CONF_NBITS, CONF_NEC, \
CONF_PANASONIC, CONF_RAW, CONF_REPEAT, CONF_SONY, CONF_TIMES, CONF_WAIT_TIME
from esphomeyaml.core import ESPHomeYAMLError
from esphomeyaml.helpers import App, ArrayInitializer, HexIntLiteral, Pvariable, \
get_variable
from esphomeyaml.helpers import App, ArrayInitializer, HexIntLiteral, get_variable
DEPENDENCIES = ['ir_transmitter']
@ -98,8 +97,7 @@ def to_code(config):
ir = get_variable(config.get(CONF_IR_TRANSMITTER_ID), IRTransmitterComponent)
send_data = exp_send_data(config)
rhs = App.register_component(ir.create_transmitter(config[CONF_NAME], send_data))
switch_ = Pvariable(DataTransmitter, config[CONF_ID], rhs)
switch.register_switch(switch_, config)
switch.register_switch(rhs, config)
BUILD_FLAGS = '-DUSE_IR_TRANSMITTER'

View File

@ -26,7 +26,7 @@ zero_to_one_float = vol.All(vol.Coerce(float), vol.Range(min=0, max=1))
positive_int = vol.All(vol.Coerce(int), vol.Range(min=0))
positive_not_null_int = vol.All(vol.Coerce(int), vol.Range(min=0, min_included=False))
ALLOWED_NAME_CHARS = u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'
ALLOWED_NAME_CHARS = u'abcdefghijklmnopqrstuvwxyz0123456789_'
RESERVED_IDS = [
# C++ keywords http://en.cppreference.com/w/cpp/keyword

View File

@ -1,8 +1,8 @@
"""Constants used by esphomeyaml."""
MAJOR_VERSION = 1
MINOR_VERSION = 5
PATCH_VERSION = '3'
MINOR_VERSION = 6
PATCH_VERSION = '0'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)

View File

View File

@ -0,0 +1,176 @@
from __future__ import print_function
import codecs
import json
import logging
import os
import random
import subprocess
try:
import tornado
import tornado.gen
import tornado.ioloop
import tornado.iostream
import tornado.process
import tornado.web
import tornado.websocket
import tornado.concurrent
except ImportError as err:
pass
from esphomeyaml import const, core, __main__
from esphomeyaml.__main__ import get_serial_ports, get_base_path, get_name
from esphomeyaml.helpers import quote
_LOGGER = logging.getLogger(__name__)
CONFIG_DIR = ''
# pylint: disable=abstract-method, arguments-differ
class EsphomeyamlCommandWebSocket(tornado.websocket.WebSocketHandler):
def __init__(self, application, request, **kwargs):
super(EsphomeyamlCommandWebSocket, self).__init__(application, request, **kwargs)
self.proc = None
self.closed = False
def on_message(self, message):
if self.proc is not None:
return
command = self.build_command(message)
_LOGGER.debug(u"WebSocket opened for command %s", [quote(x) for x in command])
self.proc = tornado.process.Subprocess(command,
stdout=tornado.process.Subprocess.STREAM,
stderr=subprocess.STDOUT)
self.proc.set_exit_callback(self.proc_on_exit)
tornado.ioloop.IOLoop.current().spawn_callback(self.redirect_stream)
@tornado.gen.coroutine
def redirect_stream(self):
while True:
try:
data = yield self.proc.stdout.read_until_regex('[\n\r]')
except tornado.iostream.StreamClosedError:
break
if data.endswith('\r') and random.randrange(100) < 90:
continue
data = data.replace('\033', '\\033')
self.write_message({'event': 'line', 'data': data})
def proc_on_exit(self, returncode):
if not self.closed:
_LOGGER.debug("Process exited with return code %s", returncode)
self.write_message({'event': 'exit', 'code': returncode})
def on_close(self):
self.closed = True
if self.proc is not None and self.proc.returncode is None:
_LOGGER.debug("Terminating process")
self.proc.proc.terminate()
def build_command(self, message):
raise NotImplementedError
class EsphomeyamlLogsHandler(EsphomeyamlCommandWebSocket):
def build_command(self, message):
js = json.loads(message)
config_file = CONFIG_DIR + '/' + js['configuration']
return ["esphomeyaml", config_file, "logs", '--serial-port', js["port"], '--escape']
class EsphomeyamlRunHandler(EsphomeyamlCommandWebSocket):
def build_command(self, message):
js = json.loads(message)
config_file = os.path.join(CONFIG_DIR, js['configuration'])
return ["esphomeyaml", config_file, "run", '--upload-port', js["port"],
'--escape', '--use-esptoolpy']
class EsphomeyamlCompileHandler(EsphomeyamlCommandWebSocket):
def build_command(self, message):
js = json.loads(message)
config_file = os.path.join(CONFIG_DIR, js['configuration'])
return ["esphomeyaml", config_file, "compile"]
class SerialPortRequestHandler(tornado.web.RequestHandler):
def get(self):
ports = get_serial_ports()
data = []
for port, desc in ports:
if port == '/dev/ttyAMA0':
# ignore RPi built-in serial port
continue
data.append({'port': port, 'desc': desc})
data.append({'port': 'OTA', 'desc': 'Over-The-Air Upload/Logs'})
self.write(json.dumps(data))
class WizardRequestHandler(tornado.web.RequestHandler):
def post(self):
from esphomeyaml import wizard
kwargs = {k: ''.join(v) for k, v in self.request.arguments.iteritems()}
config = wizard.wizard_file(**kwargs)
destination = os.path.join(CONFIG_DIR, kwargs['name'] + '.yaml')
with codecs.open(destination, 'w') as f_handle:
f_handle.write(config)
self.redirect('/')
class DownloadBinaryRequestHandler(tornado.web.RequestHandler):
def get(self):
configuration = self.get_argument('configuration')
config_file = os.path.join(CONFIG_DIR, configuration)
core.CONFIG_PATH = config_file
config = __main__.read_config(core.CONFIG_PATH)
name = get_name(config)
path = os.path.join(get_base_path(config), '.pioenvs', name, 'firmware.bin')
self.set_header('Content-Type', 'application/octet-stream')
self.set_header("Content-Disposition", 'attachment; filename="{}.bin"'.format(name))
with open(path, 'rb') as f:
while 1:
data = f.read(16384) # or some other nice-sized chunk
if not data:
break
self.write(data)
self.finish()
class MainRequestHandler(tornado.web.RequestHandler):
def get(self):
files = [f for f in os.listdir(CONFIG_DIR) if f.endswith('.yaml')]
full_path_files = [os.path.join(CONFIG_DIR, f) for f in files]
self.render("templates/index.html", files=files, full_path_files=full_path_files,
version=const.__version__)
def make_app():
static_path = os.path.join(os.path.dirname(__file__), 'static')
return tornado.web.Application([
(r"/", MainRequestHandler),
(r"/logs", EsphomeyamlLogsHandler),
(r"/run", EsphomeyamlRunHandler),
(r"/compile", EsphomeyamlCompileHandler),
(r"/download.bin", DownloadBinaryRequestHandler),
(r"/serial-ports", SerialPortRequestHandler),
(r"/wizard.html", WizardRequestHandler),
(r'/static/(.*)', tornado.web.StaticFileHandler, {'path': static_path}),
], debug=True)
def start_web_server(args):
global CONFIG_DIR
CONFIG_DIR = args.configuration
if not os.path.exists(CONFIG_DIR):
os.makedirs(CONFIG_DIR)
_LOGGER.info("Starting HassIO add-on web server on port %s and configuration dir %s...",
args.port, CONFIG_DIR)
app = make_app()
app.listen(args.port)
try:
tornado.ioloop.IOLoop.current().start()
except KeyboardInterrupt:
_LOGGER.info("Shutting down...")

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,731 @@
<!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>
<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;
}
.chip {
height: 26px;
font-size: 12px;
line-height: 26px;
}
.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;
word-wrap: normal;
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: 90%;
max-height: 85%;
height: 80% !important;
}
.log {
}
.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: 19px;
}
</style>
</head>
<body>
<header>
<nav>
<div class="nav-wrapper indigo">
<a href="#" class="brand-logo left">esphomeyaml Dashboard</a>
</div>
</nav>
<div class="ribbon"></div>
</header>
<main>
<div class="container">
{% for file, full_path in zip(files, full_path_files) %}
<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(file) }}</span>
<p>
Full path: <code class="inlinecode">{{ escape(full_path) }}</code>
</p>
</div>
<div class="card-action">
<a href="#" class="action-upload" data-node="{{ file }}">Upload</a>
<a href="#" class="action-compile" data-node="{{ file }}">Compile</a>
<a href="#" class="action-show-logs" data-node="{{ file }}">Show Logs</a>
</div>
</div>
</div>
</div>
</div>
{% end %}
</div>
<div id="modal-logs" class="modal modal-fixed-footer">
<div class="modal-content">
<h4>Show Logs</h4>
<div class="upload-port row">
<div class="col s12">
<h5>Found multiple serial ports, please choose one:</h5>
</div>
<div class="input-field col s8">
<select></select>
</div>
<div class="col s4 select-port-container">
<button class="btn waves-effect waves-light upload-port-submit" type="submit" name="action">Select
<i class="material-icons right">send</i>
</button>
</div>
</div>
<div class="log-container">
<pre class="log"></pre>
</div>
</div>
<div class="modal-footer">
<a class="modal-close waves-effect waves-green btn-flat">Close</a>
</div>
</div>
<div id="modal-upload" class="modal modal-fixed-footer">
<div class="modal-content">
<h4>Compile And Upload</h4>
<div class="upload-port row">
<div class="col s12">
<h5>Found multiple upload options, please choose one:</h5>
</div>
<div class="input-field col s8">
<select></select>
</div>
<div class="col s4 select-port-container">
<button class="btn waves-effect waves-light upload-port-submit" type="submit" name="action">Select
<i class="material-icons right">send</i>
</button>
</div>
</div>
<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</a>
</div>
</div>
<div id="modal-compile" class="modal modal-fixed-footer">
<div class="modal-content">
<h4>Compile</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</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="http://nodemcu.com/index_en.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.
It may only contain the 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). Note that the ESP32 is currently
unsupported if HassIO is running on a Raspberry Pi.
</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 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">
<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 awesome
<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> (Note
<code class="inlinecode">hassio.local</code> often doesn't work, please use a static IP).
Please also specify the MQTT username and password you wish esphomelib to use
(leave them empty if you're not using any authentication).
</p>
<div class="input-field col s12">
<input id="mqtt_broker" class="validate" type="text" name="broker" required>
<label for="mqtt_broker">MQTT Broker</label>
</div>
<div class="input-field col s6">
<input id="mqtt_username" class="validate" type="text" name="mqtt_username">
<label for="mqtt_username">MQTT Username</label>
</div>
<div class="input-field col s6">
<input id="mqtt_password" class="validate" name="mqtt_password" type="password">
<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">&lt;HASS_CONFIG_FOLDER&gt;/esphomeyaml/&lt;NAME_OF_NODE&gt;.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#using-with" 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>
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 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>
<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" 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="https://esphomelib.com/esphomeyaml/index.html" target="_blank">esphomeyaml {{ version }} Documentation</a>
</div>
</div>
</footer>
<script>
document.addEventListener('DOMContentLoaded', () => {
M.AutoInit(document.body);
});
const colorReplace = (input) => {
input = input.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
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 = "";
const ws_url = 'ws://' + window.location.hostname + ':' + window.location.port;
const logsModalElem = document.getElementById("modal-logs");
const logsPortSelect = logsModalElem.querySelector('select');
const logsPortDiv = logsModalElem.querySelector(".upload-port");
const logsPortSubmit = logsModalElem.querySelector('.upload-port-submit');
let logsStart = undefined;
logsPortSubmit.addEventListener('click', () => {
const inst = M.FormSelect.getInstance(logsPortSelect);
logsStart(inst.getSelectedValues()[0]);
inst.destroy();
});
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 = "";
if (M.FormSelect.getInstance(logsPortSelect) !== undefined) {
M.FormSelect.getInstance(logsPortSelect).destroy();
}
modalInstance.open();
if (logsPortDiv.classList.contains('hide')) {
logsPortDiv.classList.remove('hide');
}
logsStart = (port) => {
logsPortDiv.classList.add('hide');
const logSocket = new WebSocket(ws_url + "/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") {
M.toast({html: `Program exited with code ${data.code}`});
}
});
logSocket.addEventListener('open', () => {
const msg = JSON.stringify({configuration: configuration, port: port});
logSocket.send(msg);
});
logSocket.addEventListener('close', () => {
M.toast({html: 'Terminated process.'});
});
modalInstance.options.onCloseStart = () => {
logSocket.close();
};
};
fetch('/serial-ports').then(res => res.json())
.then(response => {
if (response.length > 1) {
logsPortSelect.innerHTML = "";
for (let i = 0; i < response.length; i++) {
const val = response[i];
logsPortSelect.innerHTML += `<option value="${val.port}">${val.port} (${val.desc})</option>`;
}
M.FormSelect.init(logsPortSelect, {});
} else {
logsStart("OTA");
}
});
});
});
const uploadModalElem = document.getElementById("modal-upload");
const uploadPortSelect = uploadModalElem.querySelector('select');
const uploadPortDiv = uploadModalElem.querySelector(".upload-port");
const uploadPortSubmit = uploadModalElem.querySelector('.upload-port-submit');
let uploadStart = undefined;
uploadPortSubmit.addEventListener('click', () => {
const inst = M.FormSelect.getInstance(uploadPortSelect);
uploadStart(inst.getSelectedValues()[0]);
inst.destroy();
});
document.querySelectorAll(".action-upload").forEach((showLogs) => {
showLogs.addEventListener('click', (e) => {
configuration = e.target.getAttribute('data-node');
const modalInstance = M.Modal.getInstance(uploadModalElem);
const log = uploadModalElem.querySelector(".log");
log.innerHTML = "";
if (M.FormSelect.getInstance(uploadPortSelect) !== undefined) {
M.FormSelect.getInstance(uploadPortSelect).destroy();
}
modalInstance.open();
if (uploadPortDiv.classList.contains('hide')) {
uploadPortDiv.classList.remove('hide');
}
uploadStart = (port) => {
uploadPortDiv.classList.add('hide');
const logSocket = new WebSocket(ws_url + "/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") {
M.toast({html: `Program exited with code ${data.code}`});
}
});
logSocket.addEventListener('open', () => {
const msg = JSON.stringify({configuration: configuration, port: port});
logSocket.send(msg);
});
logSocket.addEventListener('close', () => {
M.toast({html: 'Terminated process.'});
});
modalInstance.options.onCloseStart = () => {
logSocket.close();
};
};
fetch('/serial-ports').then(res => res.json())
.then(response => {
if (response.length > 1) {
uploadPortSelect.innerHTML = "";
for (let i = 0; i < response.length; i++) {
const val = response[i];
uploadPortSelect.innerHTML += `<option value="${val.port}">${val.port} (${val.desc})</option>`;
}
M.FormSelect.init(uploadPortSelect, {});
} else {
uploadStart("OTA");
}
});
});
});
const compileModalElem = document.getElementById("modal-compile");
const downloadButton = compileModalElem.querySelector('.download-binary');
document.querySelectorAll(".action-compile").forEach((showLogs) => {
showLogs.addEventListener('click', (e) => {
configuration = e.target.getAttribute('data-node');
const modalInstance = M.Modal.getInstance(compileModalElem);
const log = compileModalElem.querySelector(".log");
log.innerHTML = "";
downloadButton.classList.add('disabled');
modalInstance.open();
const logSocket = new WebSocket(ws_url + "/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") {
M.toast({html: `Program exited with code ${data.code}`});
if (data.code === 0) {
downloadButton.classList.remove('disabled');
}
}
});
logSocket.addEventListener('open', () => {
const msg = JSON.stringify({configuration: configuration});
logSocket.send(msg);
});
logSocket.addEventListener('close', () => {
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 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(files) == 0 %}
<script>
document.addEventListener('DOMContentLoaded', () => {
const tapTargetElem = document.querySelector('.tap-target');
const tapTargetInstance = M.TapTarget.getInstance(tapTargetElem);
tapTargetInstance.options.onOpen = () => {
$('.tap-target-origin').on('click', () => {
startWizard();
});
};
tapTargetInstance.open();
});
</script>
{% end %}
</body>
</html>

View File

@ -39,7 +39,7 @@ def initialize(config, subscriptions, on_message, username, password, client_id)
return 0
def show_logs(config, topic=None, username=None, password=None, client_id=None):
def show_logs(config, topic=None, username=None, password=None, client_id=None, escape=False):
if topic is not None:
pass # already have topic
elif CONF_MQTT in config:
@ -57,7 +57,10 @@ def show_logs(config, topic=None, username=None, password=None, client_id=None):
def on_message(client, userdata, msg):
time = datetime.now().time().strftime(u'[%H:%M:%S]')
print(time + msg.payload)
message = msg.payload.decode('utf-8')
if escape:
message = message.replace('\033', '\\033')
print(time + message)
return initialize(config, [topic], on_message, username, password, client_id)

View File

@ -70,6 +70,18 @@ logger:
"""
def wizard_file(**kwargs):
config = BASE_CONFIG.format(**kwargs)
if kwargs['ota_password']:
config += "ota:\n password: '{}'\n".format(kwargs['ota_password'])
else:
config += "ota:\n"
return config
if os.getenv('ESPHOMEYAML_QUICKWIZARD', False):
def sleep(time):
pass
@ -272,14 +284,10 @@ def wizard(path):
print("Press ENTER for no password")
ota_password = raw_input(color('bold_white', '(password): '))
config = BASE_CONFIG.format(name=name, platform=platform, board=board,
config = wizard_file(name=name, platform=platform, board=board,
ssid=ssid, psk=psk, broker=broker,
mqtt_username=mqtt_username, mqtt_password=mqtt_password)
if ota_password:
config += "ota:\n password: '{}'\n".format(ota_password)
else:
config += "ota:\n"
mqtt_username=mqtt_username, mqtt_password=mqtt_password,
ota_password=ota_password)
with codecs.open(path, 'w') as f_handle:
f_handle.write(config)