From b8c55c50438d43d50dad71acaa31c87615a8a3e5 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 21 May 2018 14:41:15 +0200 Subject: [PATCH] Updates --- esphomeyaml/__main__.py | 57 +++-- esphomeyaml/config_validation.py | 2 +- esphomeyaml/hassio/hassio.py | 57 ++++- esphomeyaml/hassio/templates/index.html | 314 ++++++++++++++++-------- 4 files changed, 305 insertions(+), 125 deletions(-) diff --git a/esphomeyaml/__main__.py b/esphomeyaml/__main__.py index 5fd3c51d7b..a7cb0f9e5b 100644 --- a/esphomeyaml/__main__.py +++ b/esphomeyaml/__main__.py @@ -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__) @@ -66,7 +67,7 @@ def choose_serial_port(config): return result[opt][0] -def run_platformio(*cmd): +def run_platformio(*cmd, **kwargs): def mock_exit(return_code): raise SystemExit(return_code) @@ -75,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: - import platformio.__main__ + main = kwargs.get('main') + if main is None: + import platformio.__main__ + main = platformio.__main__.main sys.argv = list(cmd) sys.exit = mock_exit - return platformio.__main__.main() + return main() or 0 except KeyboardInterrupt: return 1 except SystemExit as err: @@ -91,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): @@ -162,9 +172,21 @@ def get_upload_host(config): 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') + 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 != '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) @@ -190,7 +212,7 @@ def upload_program(config, args, port): def show_logs(config, args, port, escape=False): if port != 'OTA': - run_miniterm(config, port) + run_miniterm(config, port, escape=escape) return 0 return mqtt.show_logs(config, args.topic, args.username, args.password, args.client_id, escape=escape) @@ -329,6 +351,9 @@ def parse_args(argv): 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.') @@ -354,6 +379,8 @@ def parse_args(argv): 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.") @@ -408,6 +435,8 @@ def main(): except ESPHomeYAMLError as e: _LOGGER.error(e) return 1 + except KeyboardInterrupt: + return 1 if __name__ == "__main__": diff --git a/esphomeyaml/config_validation.py b/esphomeyaml/config_validation.py index 3ca59c6012..79ba441d20 100644 --- a/esphomeyaml/config_validation.py +++ b/esphomeyaml/config_validation.py @@ -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 diff --git a/esphomeyaml/hassio/hassio.py b/esphomeyaml/hassio/hassio.py index e7f3d2a648..114a916471 100644 --- a/esphomeyaml/hassio/hassio.py +++ b/esphomeyaml/hassio/hassio.py @@ -1,9 +1,9 @@ from __future__ import print_function import codecs -import os import json import logging +import os import random import subprocess @@ -19,8 +19,8 @@ try: except ImportError as err: pass -from esphomeyaml import const -from esphomeyaml.__main__ import get_serial_ports +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__) @@ -76,8 +76,9 @@ class EsphomeyamlLogsHandler(EsphomeyamlCommandWebSocket): super(EsphomeyamlLogsHandler, self).__init__(application, request, **kwargs) def build_command(self, message): - config_file = CONFIG_DIR + '/' + message - return ["esphomeyaml", config_file, "logs", '--escape', '--serial-port', 'OTA'] + js = json.loads(message) + config_file = CONFIG_DIR + '/' + js['configuration'] + return ["esphomeyaml", config_file, "logs", '--serial-port', js["port"], '--escape'] class EsphomeyamlRunHandler(EsphomeyamlCommandWebSocket): @@ -86,8 +87,19 @@ class EsphomeyamlRunHandler(EsphomeyamlCommandWebSocket): def build_command(self, message): js = json.loads(message) - config_file = CONFIG_DIR + '/' + js['configuration'] - return ["esphomeyaml", config_file, "run", '--upload-port', js["port"], '--escape'] + 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 __init__(self, application, request, **kwargs): + super(EsphomeyamlCompileHandler, self).__init__(application, request, **kwargs) + + 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): @@ -107,7 +119,7 @@ class WizardRequestHandler(tornado.web.RequestHandler): def post(self): from esphomeyaml import wizard - kwargs = {k:''.join(v) for k,v in self.request.arguments.iteritems()} + 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: @@ -116,10 +128,31 @@ class WizardRequestHandler(tornado.web.RequestHandler): 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')] - self.render("templates/index.html", files=files, version=const.__version__) + 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(): @@ -128,15 +161,19 @@ def make_app(): (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) diff --git a/esphomeyaml/hassio/templates/index.html b/esphomeyaml/hassio/templates/index.html index 99c691923f..45fb1966c4 100644 --- a/esphomeyaml/hassio/templates/index.html +++ b/esphomeyaml/hassio/templates/index.html @@ -72,6 +72,16 @@ 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; } @@ -112,20 +122,11 @@ .modal { width: 90%; max-height: 85%; - } - - .log-container { - position: absolute; - bottom: 0; - width: calc(100% - 48px); + height: 80% !important; } .log { - position: absolute; - bottom: 0; - right: 0; - left: 0; - overflow-y: auto; + } .page-footer { @@ -155,6 +156,10 @@ 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; + } @@ -171,7 +176,7 @@
- {% for file in files %} + {% for file, full_path in zip(files, full_path_files) %}
@@ -180,10 +185,14 @@
- {{ file }} + {{ escape(file) }} +

+ Full path: {{ escape(full_path) }} +

@@ -196,23 +205,14 @@ - - @@ -278,7 +316,8 @@

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). + 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.

@@ -383,7 +424,8 @@
  • Flash the firmware. This can be done using the “UPLOAD” option in the dashboard. See this - for guides on how to flash different types of devices. + 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.
  • See the esphomeyaml index @@ -468,68 +510,44 @@ let configuration = ""; const ws_url = 'ws://' + window.location.hostname + ':' + window.location.port; - document.querySelectorAll(".action-show-logs").forEach((showLogs) => { - console.log(showLogs); - showLogs.addEventListener('click', (e) => { - configuration = e.target.getAttribute('data-node'); - const elem = document.getElementById("modal-logs"); - const instance = M.Modal.getInstance(elem); - const log = elem.querySelector(".log"); - log.innerHTML = ""; + 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; - instance.open(); - - 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); - } - }); - logSocket.addEventListener('open', (event) => { - logSocket.send(configuration); - }); - logSocket.addEventListener('close', () => { - M.toast({html: 'Terminated process.'}); - }); - - instance.options.onCloseStart = () => { - logSocket.close(); - }; - }); + logsPortSubmit.addEventListener('click', () => { + const inst = M.FormSelect.getInstance(logsPortSelect); + logsStart(inst.getSelectedValues()[0]); + inst.destroy(); }); - const modalRunElem = document.getElementById("modal-run"); - const submitButton = modalRunElem.querySelector('.upload-port-submit'); - let startRun = undefined; - const select = modalRunElem.querySelector('select'); - const uploadPort = modalRunElem.querySelector(".upload-port"); - - document.querySelectorAll(".action-upload").forEach((actionUpload) => { - actionUpload.addEventListener('click', (e) => { + document.querySelectorAll(".action-show-logs").forEach((showLogs) => { + showLogs.addEventListener('click', (e) => { configuration = e.target.getAttribute('data-node'); - - const modal_instance = M.Modal.getInstance(modalRunElem); - if (M.FormSelect.getInstance(select) === undefined) { - M.FormSelect.getInstance(select).destroy(); - } - const log = modalRunElem.querySelector(".log"); + const modalInstance = M.Modal.getInstance(logsModalElem); + const log = logsModalElem.querySelector(".log"); log.innerHTML = ""; - if (uploadPort.classList.contains('hide')) { - uploadPort.classList.remove('hide'); + if (M.FormSelect.getInstance(logsPortSelect) !== undefined) { + M.FormSelect.getInstance(logsPortSelect).destroy(); + } + modalInstance.open(); + + if (logsPortDiv.classList.contains('hide')) { + logsPortDiv.classList.remove('hide'); } - modal_instance.open(); - - startRun = (port) => { - const logSocket = new WebSocket(ws_url + "/run"); + 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', () => { @@ -539,7 +557,7 @@ logSocket.addEventListener('close', () => { M.toast({html: 'Terminated process.'}); }); - modal_instance.options.onCloseStart = () => { + modalInstance.options.onCloseStart = () => { logSocket.close(); }; }; @@ -547,26 +565,130 @@ fetch('/serial-ports').then(res => res.json()) .then(response => { if (response.length > 1) { - select.innerHTML = ""; + logsPortSelect.innerHTML = ""; for (let i = 0; i < response.length; i++) { const val = response[i]; - select.innerHTML += ``; + logsPortSelect.innerHTML += ``; } - M.FormSelect.init(select, {}); + M.FormSelect.init(logsPortSelect, {}); } else { - uploadPort.classList.add('hide'); - startRun("OTA"); + logsStart("OTA"); } }); }); }); - submitButton.addEventListener('click', () => { - const inst = M.FormSelect.getInstance(select); - console.log(inst.getSelectedValues()); - startRun(inst.getSelectedValues()[0]); + 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(); - uploadPort.classList.add('hide'); + }); + + 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 += ``; + } + 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"); @@ -586,14 +708,6 @@ showFeedbackLoader: true, parallel: false }); - - document.getElementById('#step-1-continue').addEventListener('click', () => { - console.log("NEXT STEP"); - $('.stepper').nextStep(); - }); - $('#step-1-continue').on('click', () => { - - }); }; setupWizardStart.addEventListener('click', startWizard);