mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	HassIO add-on (#18)
* HassIO Beginnings * Updates * Fix pylint errors * Fix pylint error
This commit is contained in:
		| @@ -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()) | ||||
|   | ||||
| @@ -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' | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
							
								
								
									
										0
									
								
								esphomeyaml/hassio/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphomeyaml/hassio/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										176
									
								
								esphomeyaml/hassio/hassio.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								esphomeyaml/hassio/hassio.py
									
									
									
									
									
										Normal 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...") | ||||
							
								
								
									
										5
									
								
								esphomeyaml/hassio/static/materialize-stepper.min.css
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										5
									
								
								esphomeyaml/hassio/static/materialize-stepper.min.css
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										5
									
								
								esphomeyaml/hassio/static/materialize-stepper.min.js
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										5
									
								
								esphomeyaml/hassio/static/materialize-stepper.min.js
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										731
									
								
								esphomeyaml/hassio/templates/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										731
									
								
								esphomeyaml/hassio/templates/index.html
									
									
									
									
									
										Normal 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"><HASS_CONFIG_FOLDER>/esphomeyaml/<NAME_OF_NODE>.yaml</code> and | ||||
|             you will be able to edit this file with the | ||||
|             <a href="https://www.home-assistant.io/addons/configurator/" target="_blank">HASS Configuratior add-on</a>. | ||||
|           </p> | ||||
|           <h5>Next steps</h5> | ||||
|           <ul class="browser-default"> | ||||
|             <li> | ||||
|               Flash the firmware. This can be done using the “UPLOAD” option in the dashboard. See | ||||
|               <a href="https://esphomelib.com/esphomeyaml/index.html#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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"); | ||||
|     input = input.replace(/\\033\[(?:0;)?31m/g, '<span class="e">'); | ||||
|     input = input.replace(/\\033\[(?:1;)?31m/g, '<span class="e bold">'); | ||||
|     input = input.replace(/\\033\[(?:0;)?32m/g, '<span class="i">'); | ||||
|     input = input.replace(/\\033\[(?:1;)?32m/g, '<span class="i bold">'); | ||||
|     input = input.replace(/\\033\[(?:0;)?33m/g, '<span class="w">'); | ||||
|     input = input.replace(/\\033\[(?:1;)?33m/g, '<span class="w bold">'); | ||||
|     input = input.replace(/\\033\[(?:0;)?35m/g, '<span class="c">'); | ||||
|     input = input.replace(/\\033\[(?:1;)?35m/g, '<span class="c bold">'); | ||||
|     input = input.replace(/\\033\[(?:0;)?36m/g, '<span class="d">'); | ||||
|     input = input.replace(/\\033\[(?:1;)?36m/g, '<span class="d bold">'); | ||||
|     input = input.replace(/\\033\[(?:0;)?37m/g, '<span class="v">'); | ||||
|     input = input.replace(/\\033\[(?:1;)?37m/g, '<span class="v bold">'); | ||||
|     input = input.replace(/\\033\[(?:0;)?38m/g, '<span class="vv">'); | ||||
|     input = input.replace(/\\033\[(?:1;)?38m/g, '<span class="vv bold">'); | ||||
|     input = input.replace(/\\033\[0m/g, '</span>'); | ||||
|  | ||||
|     return input; | ||||
|   }; | ||||
|  | ||||
|   let configuration = ""; | ||||
|   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> | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user