From 94d7ac4ef06ed6d8a006e6f740963ab229096487 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 21 May 2018 16:40:22 +0200 Subject: [PATCH] HassIO add-on (#18) * HassIO Beginnings * Updates * Fix pylint errors * Fix pylint error --- MANIFEST.in | 4 + esphomeyaml/__main__.py | 280 ++++--- .../components/switch/ir_transmitter.py | 8 +- esphomeyaml/config_validation.py | 2 +- esphomeyaml/const.py | 4 +- esphomeyaml/hassio/__init__.py | 0 esphomeyaml/hassio/hassio.py | 176 +++++ .../hassio/static/materialize-stepper.min.css | 5 + .../hassio/static/materialize-stepper.min.js | 5 + esphomeyaml/hassio/templates/index.html | 731 ++++++++++++++++++ esphomeyaml/mqtt.py | 7 +- esphomeyaml/wizard.py | 24 +- 12 files changed, 1140 insertions(+), 106 deletions(-) create mode 100644 MANIFEST.in create mode 100644 esphomeyaml/hassio/__init__.py create mode 100644 esphomeyaml/hassio/hassio.py create mode 100755 esphomeyaml/hassio/static/materialize-stepper.min.css create mode 100755 esphomeyaml/hassio/static/materialize-stepper.min.js create mode 100644 esphomeyaml/hassio/templates/index.html diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000000..155442d556 --- /dev/null +++ b/MANIFEST.in @@ -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 diff --git a/esphomeyaml/__main__.py b/esphomeyaml/__main__.py index 8b1780bef8..2bd1b9759a 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__) @@ -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 - + from serial.tools.list_ports import comports 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: - import platformio.__main__ + 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()) diff --git a/esphomeyaml/components/switch/ir_transmitter.py b/esphomeyaml/components/switch/ir_transmitter.py index 0b9c21f04a..0dd6144a5b 100644 --- a/esphomeyaml/components/switch/ir_transmitter.py +++ b/esphomeyaml/components/switch/ir_transmitter.py @@ -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' 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/const.py b/esphomeyaml/const.py index 7e7395f1e8..1ec76cacd3 100644 --- a/esphomeyaml/const.py +++ b/esphomeyaml/const.py @@ -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) diff --git a/esphomeyaml/hassio/__init__.py b/esphomeyaml/hassio/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphomeyaml/hassio/hassio.py b/esphomeyaml/hassio/hassio.py new file mode 100644 index 0000000000..4b43358cc1 --- /dev/null +++ b/esphomeyaml/hassio/hassio.py @@ -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...") diff --git a/esphomeyaml/hassio/static/materialize-stepper.min.css b/esphomeyaml/hassio/static/materialize-stepper.min.css new file mode 100755 index 0000000000..753e3121b2 --- /dev/null +++ b/esphomeyaml/hassio/static/materialize-stepper.min.css @@ -0,0 +1,5 @@ +/* Materializecss Stepper - By Kinark 2016 +// https://github.com/Kinark/Materialize-stepper +// CSS.min v2.1.3 +*/ +label.invalid{font-size:12.8px;font-size:.8rem;font-weight:500;color:red!important;top:50px!important}label.invalid.active{-webkit-transform:translateY(0)!important;transform:translateY(0)!important}ul.stepper{counter-reset:section;overflow-y:auto;overflow-x:hidden}.card-content ul.stepper{margin:1em -24px;padding:0 24px}@media only screen and (min-width:993px){ul.stepper.horizontal{position:relative;display:-ms-flexbox;display:-webkit-box;display:flex;-ms-flex-pack:justify;-webkit-box-pack:justify;justify-content:space-between;min-height:458px}.card-content ul.stepper.horizontal{margin-left:-24px;margin-right:-24px;padding-left:24px;padding-right:24px;overflow:hidden}.card-content ul.stepper.horizontal:first-child{margin-top:-24px}ul.stepper.horizontal:before{content:'';background-color:transparent;width:100%;min-height:84px;box-shadow:0 2px 1px -1px rgba(0,0,0,.2),0 1px 1px 0 rgba(0,0,0,.14),0 1px 3px 0 rgba(0,0,0,.12);position:absolute;left:0}}ul.stepper .wait-feedback{left:0;right:0;top:0;z-index:2;position:absolute;width:100%;height:100%;text-align:center;display:-ms-flexbox;display:-webkit-box;display:flex;-ms-flex-pack:center;-webkit-box-pack:center;justify-content:center;-ms-flex-align:center;-webkit-box-align:center;align-items:center}ul.stepper .step{position:relative}ul.stepper .step.feedbacking .step-content>:not(.wait-feedback){opacity:.1;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=10)"}@media only screen and (min-width:993px){ul.stepper.horizontal .step{position:static;margin:0;width:100%;display:-ms-flexbox;display:-webkit-box;display:flex;-ms-flex-align:center;-webkit-box-align:center;align-items:center;height:84px!important}ul.stepper.horizontal>.step:last-of-type,ul.stepper.horizontal>.step[data-last=true]{width:auto!important}ul.stepper.horizontal .step:not(:last-of-type):after,ul.stepper.horizontal>.step.active:not(:last-of-type):after{content:'';position:static;display:inline-block;width:100%;height:1px}}ul.stepper>li:not(:last-of-type){margin-bottom:10px;-webkit-transition:margin-bottom .4s;transition:margin-bottom .4s}ul.stepper .step:not(:last-of-type).active{margin-bottom:36px}ul.stepper .step:before{position:absolute;top:12px;counter-increment:section;content:counter(section);height:28px;width:28px;color:#fff;background-color:rgba(0,0,0,.3);border-radius:50%;text-align:center;line-height:28px;font-weight:400}ul.stepper .step.active:before,ul.stepper .step.done:before{background-color:#2196f3}ul.stepper .step.done:before{content:'\e5ca';font-size:16px;font-family:'Material Icons'}ul.stepper .step.wrong:before{content:'\e001';font-size:24px;font-family:'Material Icons';background-color:red!important}ul.stepper .step-title{margin:0 -24px;cursor:pointer;padding:15.5px 44px 24px 64px;display:block}@media only screen and (min-width:993px){ul.stepper.horizontal .step.active .step-title:before,ul.stepper.horizontal .step.done .step-title:before{background-color:#2196f3}ul.stepper.horizontal .step.done .step-title:before{content:'\e5ca';font-size:16px;font-family:'Material Icons'}ul.stepper.horizontal .step.wrong .step-title:before{content:'\e001';font-size:24px;font-family:'Material Icons';background-color:red!important}ul.stepper.horizontal .step-title{line-height:84px;height:84px;margin:0;padding:0 25px 0 65px;display:inline-block;max-width:220px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;-ms-flex-negative:0;flex-shrink:0}ul.stepper.horizontal .step:before{content:none}ul.stepper.horizontal .step .step-title:before{position:absolute;top:28.5px;left:19px;counter-increment:section;content:counter(section);height:28px;width:28px;color:#fff;background-color:rgba(0,0,0,.3);border-radius:50%;text-align:center;line-height:28px;font-weight:400}ul.stepper.horizontal .step-title:after{top:15px}}ul.stepper .step-title:after{content:attr(data-step-label);display:block;position:absolute;font-size:12.8px;font-size:.8rem;color:#424242;font-weight:400}ul.stepper .step-title:hover{background-color:rgba(0,0,0,.06)}ul.stepper .step.active .step-title{font-weight:500}ul.stepper .step-content{position:relative;display:none;height:calc(100% - 132px);width:inherit;overflow:visible;margin-left:41px;margin-right:24px}@media only screen and (min-width:993px){ul.stepper.horizontal .step-content{position:absolute;height:calc(100% - 84px);top:84px;left:0;width:100%;overflow-y:auto;overflow-x:hidden;margin:0;padding:20px 20px 76px}.card-content ul.stepper.horizontal .step-content{padding-left:40px;padding-right:40px}}ul.stepper>.step:not(:last-of-type):after{content:'';position:absolute;top:50px;left:13.5px;width:1px;height:40%;height:calc(100% - 38px);background-color:rgba(0,0,0,.1);-webkit-transition:all .4s;transition:all .4s}ul.stepper>.step.active:not(:last-child):after{height:93%;height:calc(100% - 12px)}ul.stepper>.step[data-last=true]:after{height:0;width:0}ul.stepper>.step[data-last=true]{margin-bottom:0}ul.stepper .step-actions{padding-top:16px;-webkit-display:flex;-moz-display:flex;-ms-display:flex;display:-webkit-box;display:flex;-ms-flex-pack:start;-webkit-box-pack:start;justify-content:flex-start}ul.stepper .step-actions .btn-flat:not(:last-child),ul.stepper .step-actions .btn-large:not(:last-child),ul.stepper .step-actions .btn:not(:last-child){margin-right:5px}@media only screen and (min-width:993px){ul.stepper.horizontal .step-actions .btn-flat:not(:last-child),ul.stepper.horizontal .step-actions .btn-large:not(:last-child),ul.stepper.horizontal .step-actions .btn:not(:last-child){margin-left:5px;margin-right:0}ul.stepper.horizontal .step-actions{position:absolute;bottom:0;left:0;width:100%;padding:20px;background-color:#fff;-ms-flex-direction:row-reverse;-webkit-box-orient:horizontal;-webkit-box-direction:reverse;flex-direction:row-reverse}.card-content ul.stepper.horizontal .step-actions{padding-left:40px;padding-right:40px}}ul.stepper .step-content .row{margin-bottom:7px} diff --git a/esphomeyaml/hassio/static/materialize-stepper.min.js b/esphomeyaml/hassio/static/materialize-stepper.min.js new file mode 100755 index 0000000000..d8c12e7eef --- /dev/null +++ b/esphomeyaml/hassio/static/materialize-stepper.min.js @@ -0,0 +1,5 @@ +/* Materializecss Stepper - By Kinark 2016 +// https://github.com/Kinark/Materialize-stepper +// JS.min v2.1.3 +*/ +var validation=$.isFunction($.fn.valid)?1:0;$.fn.isValid=function(){return!validation||this.valid()},validation&&$.validator.setDefaults({errorClass:"invalid",validClass:"valid",errorPlacement:function(a,b){b.is(":radio")||b.is(":checkbox")?a.insertBefore($(b).parent()):a.insertAfter(b)},success:function(a){$(a).closest("li").find("label.invalid:not(:empty)").length||$(a).closest("li").removeClass("wrong")}}),$.fn.getActiveStep=function(){return active=this.find(".step.active"),$(this.children(".step:visible")).index($(active))+1},$.fn.activateStep=function(a){$(this).hasClass("step")||(stepper=$(this).closest("ul.stepper"),stepper.find(">li").removeAttr("data-last"),window.innerWidth<993||!stepper.hasClass("horizontal")?$(this).addClass("step").stop().slideDown(400,function(){$(this).css({height:"auto","margin-bottom":"",display:"inherit"}),a&&a(),stepper.find(">li.step").last().attr("data-last","true")}):$(this).addClass("step").stop().css({width:"0%",display:"inherit"}).animate({width:"100%"},400,function(){$(this).css({height:"auto","margin-bottom":"",display:"inherit"}),a&&a(),stepper.find(">li.step").last().attr("data-last","true")}))},$.fn.deactivateStep=function(a){$(this).hasClass("step")&&(stepper=$(this).closest("ul.stepper"),stepper.find(">li").removeAttr("data-last"),window.innerWidth<993||!stepper.hasClass("horizontal")?$(this).stop().css({transition:"none","-webkit-transition":"margin-bottom none"}).slideUp(400,function(){$(this).removeClass("step").css({height:"auto","margin-bottom":"",transition:"margin-bottom .4s","-webkit-transition":"margin-bottom .4s"}),a&&a(),stepper.find(">li").removeAttr("data-last"),stepper.find(">li.step").last().attr("data-last","true")}):$(this).stop().animate({width:"0%"},400,function(){$(this).removeClass("step").hide().css({height:"auto","margin-bottom":"",display:"none",width:""}),a&&a(),stepper.find(">li.step").last().attr("data-last","true")}))},$.fn.showError=function(a){if(validation){name=this.attr("name"),form=this.closest("form");var b={};b[name]=a,form.validate().showErrors(b),this.closest("li").addClass("wrong")}else this.removeClass("valid").addClass("invalid"),this.next().attr("data-error",a)},$.fn.activateFeedback=function(){active=this.find(".step.active:not(.feedbacking)").addClass("feedbacking").find(".step-content"),active.prepend('
')},$.fn.destroyFeedback=function(){return active=this.find(".step.active.feedbacking"),active&&(active.removeClass("feedbacking"),active.find(".wait-feedback").remove()),!0},$.fn.resetStepper=function(a){return a||(a=1),form=$(this).closest("form"),$(form)[0].reset(),Materialize.updateTextFields(),$(this).openStep(a)},$.fn.submitStepper=function(a){form=this.closest("form"),form.isValid()&&form.submit()},$.fn.nextStep=function(a,b,c){return stepper=this,settings=$(stepper).data("settings"),form=this.closest("form"),active=this.find(".step.active"),next=$(this.children(".step:visible")).index($(active))+2,feedback=active.find(".next-step").length>1?c?$(c.target).data("feedback"):void 0:active.find(".next-step").data("feedback"),form.isValid()?feedback&&b?(settings.showFeedbackLoader&&stepper.activateFeedback(),window[feedback].call()):(active.removeClass("wrong").addClass("done"),this.openStep(next,a),this.trigger("nextstep")):active.removeClass("done").addClass("wrong")},$.fn.prevStep=function(a){if(active=this.find(".step.active"),!active.hasClass("feedbacking"))return prev=$(this.children(".step:visible")).index($(active)),active.removeClass("wrong"),this.openStep(prev,a),this.trigger("prevstep")},$.fn.openStep=function(a,b){settings=$(this).closest("ul.stepper").data("settings"),$this=this,step_num=a-1,a=this.find(".step:visible:eq("+step_num+")"),a.hasClass("active")||(active=this.find(".step.active"),prev_active=next=$(this.children(".step:visible")).index($(active)),order=step_num>prev_active?1:0,active.hasClass("feedbacking")&&$this.destroyFeedback(),active.closeAction(order),a.openAction(order,function(){settings.autoFocusInput&&a.find("input:enabled:visible:first").focus(),$this.trigger("stepchange").trigger("step"+(step_num+1)),a.data("event")&&$this.trigger(a.data("event")),b&&b()}))},$.fn.closeAction=function(a,b){closable=this.removeClass("active").find(".step-content"),window.innerWidth<993||!this.closest("ul").hasClass("horizontal")?closable.stop().slideUp(300,"easeOutQuad",b):1==a?closable.animate({left:"-100%"},function(){closable.css({display:"none",left:"0%"},b)}):closable.animate({left:"100%"},function(){closable.css({display:"none",left:"0%"},b)})},$.fn.openAction=function(a,b){openable=this.removeClass("done").addClass("active").find(".step-content"),window.innerWidth<993||!this.closest("ul").hasClass("horizontal")?openable.slideDown(300,"easeOutQuad",b):1==a?openable.css({left:"100%",display:"block"}).animate({left:"0%"},b):openable.css({left:"-100%",display:"block"}).animate({left:"0%"},b)},$.fn.activateStepper=function(a){var b=$.extend({linearStepsNavigation:!0,autoFocusInput:!0,showFeedbackLoader:!0,autoFormCreation:!0},a);$(document).on("click",function(a){$(a.target).parents(".stepper").length||$(".stepper.focused").removeClass("focused")}),$(this).each(function(){var a=$(this);!a.parents("form").length&&b.autoFormCreation&&(method=a.data("method"),action=a.data("action"),method=method?method:"GET",action=action?action:"?",a.wrap('
')),a.data("settings",{linearStepsNavigation:b.linearStepsNavigation,autoFocusInput:b.autoFocusInput,showFeedbackLoader:b.showFeedbackLoader}),a.find("li.step.active").openAction(1),a.find(">li").removeAttr("data-last"),a.find(">li.step").last().attr("data-last","true"),a.on("click",".step:not(.active)",function(){object=$(a.children(".step:visible")).index($(this)),a.hasClass("linear")?b.linearStepsNavigation&&(active=a.find(".step.active"),$(a.children(".step:visible")).index($(active))+1==object?a.nextStep(void 0,!0,void 0):$(a.children(".step:visible")).index($(active))-1==object&&a.prevStep(void 0)):a.openStep(object+1)}).on("click",".next-step",function(b){b.preventDefault(),a.nextStep(void 0,!0,b)}).on("click",".previous-step",function(b){b.preventDefault(),a.prevStep(void 0)}).on("click","button:submit:not(.next-step, .previous-step)",function(b){if(b.preventDefault(),feedback=b?$(b.target).data("feedback"):void 0,form=a.closest("form"),form.isValid()){if(feedback)return stepper.activateFeedback(),window[feedback].call();form.submit()}}).on("click",function(){$(".stepper.focused").removeClass("focused"),$(this).addClass("focused")})})}; diff --git a/esphomeyaml/hassio/templates/index.html b/esphomeyaml/hassio/templates/index.html new file mode 100644 index 0000000000..45fb1966c4 --- /dev/null +++ b/esphomeyaml/hassio/templates/index.html @@ -0,0 +1,731 @@ + + + + + esphomeyaml Dashboard + + + + + + + + + + + + + + + + + + +
+ + +
+
+ +
+
+ {% for file, full_path in zip(files, full_path_files) %} +
+
+
+
+ memory +
+
+
+ {{ escape(file) }} +

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

+
+ +
+
+
+
+ {% end %} +
+ + + + + + + + + + + add + + +
+
+
Set up your first Node
+

+ 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 🎉 +

+
+
+
+ + + + + +{% if len(files) == 0 %} + +{% end %} + + + \ No newline at end of file diff --git a/esphomeyaml/mqtt.py b/esphomeyaml/mqtt.py index 9db005e5af..4e4b75a17d 100644 --- a/esphomeyaml/mqtt.py +++ b/esphomeyaml/mqtt.py @@ -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) diff --git a/esphomeyaml/wizard.py b/esphomeyaml/wizard.py index 921176d315..2f91712ae9 100644 --- a/esphomeyaml/wizard.py +++ b/esphomeyaml/wizard.py @@ -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, - 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" + config = wizard_file(name=name, platform=platform, board=board, + ssid=ssid, psk=psk, broker=broker, + mqtt_username=mqtt_username, mqtt_password=mqtt_password, + ota_password=ota_password) with codecs.open(path, 'w') as f_handle: f_handle.write(config)