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:
		
							
								
								
									
										4
									
								
								MANIFEST.in
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								MANIFEST.in
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | include README.md | ||||||
|  | include esphomeyaml/hassio/templates/index.html | ||||||
|  | include esphomeyaml/hassio/static/materialize-stepper.min.css | ||||||
|  | include esphomeyaml/hassio/static/materialize-stepper.min.js | ||||||
| @@ -1,18 +1,19 @@ | |||||||
| from __future__ import print_function | from __future__ import print_function | ||||||
|  |  | ||||||
| import argparse | import argparse | ||||||
|  | from datetime import datetime | ||||||
| import logging | import logging | ||||||
| import os | import os | ||||||
| import random | import random | ||||||
| import sys | 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.config import core_to_code, get_component, iter_components, read_config | ||||||
| from esphomeyaml.const import CONF_BAUD_RATE, CONF_DOMAIN, CONF_ESPHOMEYAML, CONF_HOSTNAME, \ | 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.core import ESPHomeYAMLError | ||||||
| from esphomeyaml.helpers import AssignmentExpression, RawStatement, _EXPRESSIONS, add, add_task, \ | from esphomeyaml.helpers import AssignmentExpression, Expression, RawStatement, _EXPRESSIONS, add, \ | ||||||
|     color, get_variable, indent, quote, statement, Expression |     add_task, color, get_variable, indent, quote, statement | ||||||
|  |  | ||||||
| _LOGGER = logging.getLogger(__name__) | _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)) |     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 |     # from https://github.com/pyserial/pyserial/blob/master/serial/tools/list_ports.py | ||||||
|     try: |     from serial.tools.list_ports import comports | ||||||
|         from serial.tools.list_ports import comports |  | ||||||
|     except ImportError: |  | ||||||
|         return None |  | ||||||
|  |  | ||||||
|     result = [] |     result = [] | ||||||
|     descs = [] |  | ||||||
|     for port, desc, info in comports(): |     for port, desc, info in comports(): | ||||||
|         if not port: |         if not port: | ||||||
|             continue |             continue | ||||||
|         if "VID:PID" in info: |         if "VID:PID" in info: | ||||||
|             result.append(port) |             result.append((port, desc)) | ||||||
|             descs.append(desc) |     return result | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def choose_serial_port(config): | ||||||
|  |     result = get_serial_ports() | ||||||
|  |  | ||||||
|     if not result: |     if not result: | ||||||
|         return None |         return 'OTA' | ||||||
|     print(u"Found multiple serial port options, please choose one:") |     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"  [{}] {} ({})".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() |     print() | ||||||
|     while True: |     while True: | ||||||
|         opt = raw_input('(number): ') |         opt = raw_input('(number): ') | ||||||
| @@ -63,11 +63,11 @@ def discover_serial_ports(): | |||||||
|         except ValueError: |         except ValueError: | ||||||
|             print(color('red', u"Invalid option: '{}'".format(opt))) |             print(color('red', u"Invalid option: '{}'".format(opt))) | ||||||
|     if opt == len(result): |     if opt == len(result): | ||||||
|         return None |         return 'OTA' | ||||||
|     return result[opt] |     return result[opt][0] | ||||||
|  |  | ||||||
|  |  | ||||||
| def run_platformio(*cmd): | def run_platformio(*cmd, **kwargs): | ||||||
|     def mock_exit(return_code): |     def mock_exit(return_code): | ||||||
|         raise SystemExit(return_code) |         raise SystemExit(return_code) | ||||||
|  |  | ||||||
| @@ -76,10 +76,13 @@ def run_platformio(*cmd): | |||||||
|     full_cmd = u' '.join(quote(x) for x in cmd) |     full_cmd = u' '.join(quote(x) for x in cmd) | ||||||
|     _LOGGER.info(u"Running:  %s", full_cmd) |     _LOGGER.info(u"Running:  %s", full_cmd) | ||||||
|     try: |     try: | ||||||
|         import platformio.__main__ |         func = kwargs.get('main') | ||||||
|  |         if func is None: | ||||||
|  |             import platformio.__main__ | ||||||
|  |             func = platformio.__main__.main | ||||||
|         sys.argv = list(cmd) |         sys.argv = list(cmd) | ||||||
|         sys.exit = mock_exit |         sys.exit = mock_exit | ||||||
|         return platformio.__main__.main() |         return func() or 0 | ||||||
|     except KeyboardInterrupt: |     except KeyboardInterrupt: | ||||||
|         return 1 |         return 1 | ||||||
|     except SystemExit as err: |     except SystemExit as err: | ||||||
| @@ -92,13 +95,19 @@ def run_platformio(*cmd): | |||||||
|         sys.exit = orig_exit |         sys.exit = orig_exit | ||||||
|  |  | ||||||
|  |  | ||||||
| def run_miniterm(config, port): | def run_miniterm(config, port, escape=False): | ||||||
|     from serial.tools import miniterm |     import serial | ||||||
|     baud_rate = config.get(CONF_LOGGER, {}).get(CONF_BAUD_RATE, 115200) |     baud_rate = config.get(CONF_LOGGER, {}).get(CONF_BAUD_RATE, 115200) | ||||||
|     sys.argv = ['miniterm', '--raw', '--exit-char', '3'] |     _LOGGER.info("Starting log output from %s with baud rate %s", port, baud_rate) | ||||||
|     miniterm.main( |  | ||||||
|         default_port=port, |     with serial.Serial(port, baudrate=baud_rate) as ser: | ||||||
|         default_baudrate=baud_rate) |         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): | def write_cpp(config): | ||||||
| @@ -153,9 +162,32 @@ def compile_program(config): | |||||||
|     return run_platformio('platformio', 'run', '-d', get_base_path(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): | def upload_program(config, args, port): | ||||||
|     _LOGGER.info("Uploading binary...") |     _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), |         return run_platformio('platformio', 'run', '-d', get_base_path(config), | ||||||
|                               '-t', 'upload', '--upload-port', port) |                               '-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!") |         _LOGGER.error("No serial port found and OTA not enabled. Can't upload!") | ||||||
|         return -1 |         return -1 | ||||||
|  |  | ||||||
|     if CONF_MANUAL_IP in config[CONF_WIFI]: |     host = get_upload_host(config) | ||||||
|         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] |  | ||||||
|  |  | ||||||
|     from esphomeyaml.components import ota |     from esphomeyaml.components import ota | ||||||
|     from esphomeyaml import espota |     from esphomeyaml import espota | ||||||
| @@ -184,11 +211,12 @@ def upload_program(config, args, port): | |||||||
|     return espota.main(espota_args) |     return espota.main(espota_args) | ||||||
|  |  | ||||||
|  |  | ||||||
| def show_logs(config, args, port): | def show_logs(config, args, port, escape=False): | ||||||
|     if port is not None and port != 'OTA': |     if port != 'OTA': | ||||||
|         run_miniterm(config, port) |         run_miniterm(config, port, escape=escape) | ||||||
|         return 0 |         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): | def clean_mqtt(config, args): | ||||||
| @@ -221,11 +249,98 @@ def setup_log(): | |||||||
|         pass |         pass | ||||||
|  |  | ||||||
|  |  | ||||||
| def main(): | def command_wizard(args): | ||||||
|     setup_log() |     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 = argparse.ArgumentParser(prog='esphomeyaml') | ||||||
|     parser.add_argument('configuration', help='Your YAML configuration file.') |     parser.add_argument('configuration', help='Your YAML configuration file.') | ||||||
|  |  | ||||||
|     subparsers = parser.add_subparsers(help='Commands', dest='command') |     subparsers = parser.add_subparsers(help='Commands', dest='command') | ||||||
|     subparsers.required = True |     subparsers.required = True | ||||||
|     subparsers.add_parser('config', help='Validate the configuration and spit it out.') |     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. " |     parser_upload.add_argument('--upload-port', help="Manually specify the upload port to use. " | ||||||
|                                                      "For example /dev/cu.SLAB_USBtoUART.") |                                                      "For example /dev/cu.SLAB_USBtoUART.") | ||||||
|     parser_upload.add_argument('--host-port', help="Specify the host port.", type=int) |     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 ' |     parser_logs = subparsers.add_parser('logs', help='Validate the configuration ' | ||||||
|                                                      'and show all MQTT logs.') |                                                      '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('--client-id', help='Manually set the client id.') | ||||||
|     parser_logs.add_argument('--serial-port', help="Manually specify a serial port to use" |     parser_logs.add_argument('--serial-port', help="Manually specify a serial port to use" | ||||||
|                                                    "For example /dev/cu.SLAB_USBtoUART.") |                                                    "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, ' |     parser_run = subparsers.add_parser('run', help='Validate the configuration, create a binary, ' | ||||||
|                                                    'upload it, and start MQTT logs.') |                                                    '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('--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('--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('--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 " |     parser_clean = subparsers.add_parser('clean-mqtt', help="Helper to clear an MQTT topic from " | ||||||
|                                                             "retain messages.") |                                                             "retain messages.") | ||||||
| @@ -270,12 +394,25 @@ def main(): | |||||||
|                                          "you through setting up esphomeyaml.") |                                          "you through setting up esphomeyaml.") | ||||||
|  |  | ||||||
|     subparsers.add_parser('mqtt-fingerprint', help="Get the SSL fingerprint from a MQTT broker.") |     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.") |     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 parser.parse_args(argv[1:]) | ||||||
|         return wizard.wizard(args.configuration) |  | ||||||
|  |  | ||||||
|  | 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 |     core.CONFIG_PATH = args.configuration | ||||||
|  |  | ||||||
| @@ -283,58 +420,25 @@ def main(): | |||||||
|     if config is None: |     if config is None: | ||||||
|         return 1 |         return 1 | ||||||
|  |  | ||||||
|     if args.command == 'config': |     if args.command in POST_CONFIG_ACTIONS: | ||||||
|         print(yaml_util.dump(config)) |  | ||||||
|         return 0 |  | ||||||
|     elif args.command == 'compile': |  | ||||||
|         try: |         try: | ||||||
|             exit_code = write_cpp(config) |             return POST_CONFIG_ACTIONS[args.command](args, config) | ||||||
|         except ESPHomeYAMLError as e: |         except ESPHomeYAMLError as e: | ||||||
|             _LOGGER.error(e) |             _LOGGER.error(e) | ||||||
|             return 1 |             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)) |     print(u"Unknown command {}".format(args.command)) | ||||||
|     return 1 |     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__": | if __name__ == "__main__": | ||||||
|     sys.exit(main()) |     sys.exit(main()) | ||||||
|   | |||||||
| @@ -4,11 +4,10 @@ import esphomeyaml.config_validation as cv | |||||||
| from esphomeyaml.components import switch | from esphomeyaml.components import switch | ||||||
| from esphomeyaml.components.ir_transmitter import IRTransmitterComponent | from esphomeyaml.components.ir_transmitter import IRTransmitterComponent | ||||||
| from esphomeyaml.const import CONF_ADDRESS, CONF_CARRIER_FREQUENCY, CONF_COMMAND, CONF_DATA, \ | 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 |     CONF_PANASONIC, CONF_RAW, CONF_REPEAT, CONF_SONY, CONF_TIMES, CONF_WAIT_TIME | ||||||
| from esphomeyaml.core import ESPHomeYAMLError | from esphomeyaml.core import ESPHomeYAMLError | ||||||
| from esphomeyaml.helpers import App, ArrayInitializer, HexIntLiteral, Pvariable, \ | from esphomeyaml.helpers import App, ArrayInitializer, HexIntLiteral, get_variable | ||||||
|     get_variable |  | ||||||
|  |  | ||||||
| DEPENDENCIES = ['ir_transmitter'] | DEPENDENCIES = ['ir_transmitter'] | ||||||
|  |  | ||||||
| @@ -98,8 +97,7 @@ def to_code(config): | |||||||
|     ir = get_variable(config.get(CONF_IR_TRANSMITTER_ID), IRTransmitterComponent) |     ir = get_variable(config.get(CONF_IR_TRANSMITTER_ID), IRTransmitterComponent) | ||||||
|     send_data = exp_send_data(config) |     send_data = exp_send_data(config) | ||||||
|     rhs = App.register_component(ir.create_transmitter(config[CONF_NAME], send_data)) |     rhs = App.register_component(ir.create_transmitter(config[CONF_NAME], send_data)) | ||||||
|     switch_ = Pvariable(DataTransmitter, config[CONF_ID], rhs) |     switch.register_switch(rhs, config) | ||||||
|     switch.register_switch(switch_, config) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| BUILD_FLAGS = '-DUSE_IR_TRANSMITTER' | 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_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)) | 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 = [ | RESERVED_IDS = [ | ||||||
|     # C++ keywords http://en.cppreference.com/w/cpp/keyword |     # C++ keywords http://en.cppreference.com/w/cpp/keyword | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| """Constants used by esphomeyaml.""" | """Constants used by esphomeyaml.""" | ||||||
|  |  | ||||||
| MAJOR_VERSION = 1 | MAJOR_VERSION = 1 | ||||||
| MINOR_VERSION = 5 | MINOR_VERSION = 6 | ||||||
| PATCH_VERSION = '3' | PATCH_VERSION = '0' | ||||||
| __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) | __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) | ||||||
| __version__ = '{}.{}'.format(__short_version__, PATCH_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 |     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: |     if topic is not None: | ||||||
|         pass  # already have topic |         pass  # already have topic | ||||||
|     elif CONF_MQTT in config: |     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): |     def on_message(client, userdata, msg): | ||||||
|         time = datetime.now().time().strftime(u'[%H:%M:%S]') |         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) |     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): | if os.getenv('ESPHOMEYAML_QUICKWIZARD', False): | ||||||
|     def sleep(time): |     def sleep(time): | ||||||
|         pass |         pass | ||||||
| @@ -272,14 +284,10 @@ def wizard(path): | |||||||
|     print("Press ENTER for no password") |     print("Press ENTER for no password") | ||||||
|     ota_password = raw_input(color('bold_white', '(password): ')) |     ota_password = raw_input(color('bold_white', '(password): ')) | ||||||
|  |  | ||||||
|     config = BASE_CONFIG.format(name=name, platform=platform, board=board, |     config = wizard_file(name=name, platform=platform, board=board, | ||||||
|                                 ssid=ssid, psk=psk, broker=broker, |                          ssid=ssid, psk=psk, broker=broker, | ||||||
|                                 mqtt_username=mqtt_username, mqtt_password=mqtt_password) |                          mqtt_username=mqtt_username, mqtt_password=mqtt_password, | ||||||
|  |                          ota_password=ota_password) | ||||||
|     if ota_password: |  | ||||||
|         config += "ota:\n  password: '{}'\n".format(ota_password) |  | ||||||
|     else: |  | ||||||
|         config += "ota:\n" |  | ||||||
|  |  | ||||||
|     with codecs.open(path, 'w') as f_handle: |     with codecs.open(path, 'w') as f_handle: | ||||||
|         f_handle.write(config) |         f_handle.write(config) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user