mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Better ANSI color escaping
This commit is contained in:
		| @@ -63,7 +63,7 @@ def choose_serial_port(config): | |||||||
|     return result[opt][0] |     return result[opt][0] | ||||||
|  |  | ||||||
|  |  | ||||||
| def run_miniterm(config, port, escape=False): | def run_miniterm(config, port): | ||||||
|     import serial |     import serial | ||||||
|     if CONF_LOGGER not in config: |     if CONF_LOGGER not in config: | ||||||
|         _LOGGER.info("Logger is not enabled. Not starting UART logs.") |         _LOGGER.info("Logger is not enabled. Not starting UART logs.") | ||||||
| @@ -84,7 +84,7 @@ def run_miniterm(config, port, escape=False): | |||||||
|             line = raw.replace('\r', '').replace('\n', '') |             line = raw.replace('\r', '').replace('\n', '') | ||||||
|             time = datetime.now().time().strftime('[%H:%M:%S]') |             time = datetime.now().time().strftime('[%H:%M:%S]') | ||||||
|             message = time + line |             message = time + line | ||||||
|             if escape: |             if core.FROM_DASHBOARD: | ||||||
|                 message = message.replace('\033', '\\033') |                 message = message.replace('\033', '\\033') | ||||||
|             safe_print(message) |             safe_print(message) | ||||||
|  |  | ||||||
| @@ -152,7 +152,7 @@ def upload_program(config, args, port): | |||||||
|     # if upload is to a serial port use platformio, otherwise assume ota |     # if upload is to a serial port use platformio, otherwise assume ota | ||||||
|     serial_port = port.startswith('/') or port.startswith('COM') |     serial_port = port.startswith('/') or port.startswith('COM') | ||||||
|     if port != 'OTA' and serial_port: |     if port != 'OTA' and serial_port: | ||||||
|         if CORE.is_esp8266 and args.use_esptoolpy: |         if CORE.is_esp8266 and args.dashboard: | ||||||
|             return upload_using_esptool(config, port) |             return upload_using_esptool(config, port) | ||||||
|         return platformio_api.run_upload(config, args.verbose, port) |         return platformio_api.run_upload(config, args.verbose, port) | ||||||
|  |  | ||||||
| @@ -187,13 +187,12 @@ def upload_program(config, args, port): | |||||||
|                                   CORE.firmware_bin) |                                   CORE.firmware_bin) | ||||||
|  |  | ||||||
|  |  | ||||||
| def show_logs(config, args, port, escape=False): | def show_logs(config, args, port): | ||||||
|     serial_port = port.startswith('/') or port.startswith('COM') |     serial_port = port.startswith('/') or port.startswith('COM') | ||||||
|     if port != 'OTA' and serial_port: |     if port != 'OTA' and serial_port: | ||||||
|         run_miniterm(config, port, escape=escape) |         run_miniterm(config, port) | ||||||
|         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): | ||||||
| @@ -282,7 +281,7 @@ def command_upload(args, config): | |||||||
|  |  | ||||||
| def command_logs(args, config): | def command_logs(args, config): | ||||||
|     port = args.serial_port or choose_serial_port(config) |     port = args.serial_port or choose_serial_port(config) | ||||||
|     return show_logs(config, args, port, escape=args.escape) |     return show_logs(config, args, port) | ||||||
|  |  | ||||||
|  |  | ||||||
| def command_run(args, config): | def command_run(args, config): | ||||||
| @@ -300,7 +299,7 @@ def command_run(args, config): | |||||||
|     _LOGGER.info(u"Successfully uploaded program.") |     _LOGGER.info(u"Successfully uploaded program.") | ||||||
|     if args.no_logs: |     if args.no_logs: | ||||||
|         return 0 |         return 0 | ||||||
|     return show_logs(config, args, port, escape=args.escape) |     return show_logs(config, args, port) | ||||||
|  |  | ||||||
|  |  | ||||||
| def command_clean_mqtt(args, config): | def command_clean_mqtt(args, config): | ||||||
| @@ -381,21 +380,24 @@ def parse_args(argv): | |||||||
|  |  | ||||||
|     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.') |     config = subparsers.add_parser('config', help='Validate the configuration and spit it out.') | ||||||
|  |     config.add_argument('--dashboard', help="Internal flag used by the dashboard", | ||||||
|  |                         action='store_true') | ||||||
|  |  | ||||||
|     parser_compile = subparsers.add_parser('compile', |     parser_compile = subparsers.add_parser('compile', | ||||||
|                                            help='Read the configuration and compile a program.') |                                            help='Read the configuration and compile a program.') | ||||||
|     parser_compile.add_argument('--only-generate', |     parser_compile.add_argument('--only-generate', | ||||||
|                                 help="Only generate source code, do not compile.", |                                 help="Only generate source code, do not compile.", | ||||||
|                                 action='store_true') |                                 action='store_true') | ||||||
|  |     parser_compile.add_argument('--dashboard', help="Internal flag used by the dashboard", | ||||||
|  |                                 action='store_true') | ||||||
|  |  | ||||||
|     parser_upload = subparsers.add_parser('upload', help='Validate the configuration ' |     parser_upload = subparsers.add_parser('upload', help='Validate the configuration ' | ||||||
|                                                          'and upload the latest binary.') |                                                          'and upload the latest binary.') | ||||||
|     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', |     parser_upload.add_argument('--dashboard', help="Internal flag used by the dashboard", | ||||||
|                                help="Use esptool.py for the uploading (only for ESP8266)", |  | ||||||
|                                action='store_true') |                                action='store_true') | ||||||
|  |  | ||||||
|     parser_logs = subparsers.add_parser('logs', help='Validate the configuration ' |     parser_logs = subparsers.add_parser('logs', help='Validate the configuration ' | ||||||
| @@ -406,7 +408,7 @@ def parse_args(argv): | |||||||
|     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 running in dashboard", |     parser_logs.add_argument('--dashboard', help="Internal flag used by the dashboard", | ||||||
|                              action='store_true') |                              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, ' | ||||||
| @@ -420,10 +422,7 @@ def parse_args(argv): | |||||||
|     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 running in dashboard", |     parser_run.add_argument('--dashboard', help="Internal flag used by the dashboard", | ||||||
|                             action='store_true') |  | ||||||
|     parser_run.add_argument('--use-esptoolpy', |  | ||||||
|                             help="Use esptool.py for the uploading (only for ESP8266)", |  | ||||||
|                             action='store_true') |                             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 " | ||||||
| @@ -432,6 +431,8 @@ def parse_args(argv): | |||||||
|     parser_clean.add_argument('--username', help='Manually set the username.') |     parser_clean.add_argument('--username', help='Manually set the username.') | ||||||
|     parser_clean.add_argument('--password', help='Manually set the password.') |     parser_clean.add_argument('--password', help='Manually set the password.') | ||||||
|     parser_clean.add_argument('--client-id', help='Manually set the client id.') |     parser_clean.add_argument('--client-id', help='Manually set the client id.') | ||||||
|  |     parser_clean.add_argument('--dashboard', help="Internal flag used by the dashboard", | ||||||
|  |                               action='store_true') | ||||||
|  |  | ||||||
|     subparsers.add_parser('wizard', help="A helpful setup wizard that will guide " |     subparsers.add_parser('wizard', help="A helpful setup wizard that will guide " | ||||||
|                                          "you through setting up esphomeyaml.") |                                          "you through setting up esphomeyaml.") | ||||||
| @@ -440,7 +441,9 @@ def parse_args(argv): | |||||||
|  |  | ||||||
|     subparsers.add_parser('version', help="Print the esphomeyaml version and exit.") |     subparsers.add_parser('version', help="Print the esphomeyaml version and exit.") | ||||||
|  |  | ||||||
|     subparsers.add_parser('clean', help="Delete all temporary build files.") |     clean = subparsers.add_parser('clean', help="Delete all temporary build files.") | ||||||
|  |     clean.add_argument('--dashboard', help="Internal flag used by the dashboard", | ||||||
|  |                        action='store_true') | ||||||
|  |  | ||||||
|     dashboard = subparsers.add_parser('dashboard', |     dashboard = subparsers.add_parser('dashboard', | ||||||
|                                       help="Create a simple web server for a dashboard.") |                                       help="Create a simple web server for a dashboard.") | ||||||
| @@ -455,14 +458,20 @@ def parse_args(argv): | |||||||
|                                 "add-on.", |                                 "add-on.", | ||||||
|                            action="store_true") |                            action="store_true") | ||||||
|  |  | ||||||
|     subparsers.add_parser('hass-config', help="Dump the configuration entries that should be added" |     hass_config = subparsers.add_parser('hass-config', | ||||||
|                                               "to Home Assistant when not using MQTT discovery.") |                                         help="Dump the configuration entries that should be added " | ||||||
|  |                                              "to Home Assistant when not using MQTT discovery.") | ||||||
|  |     hass_config.add_argument('--dashboard', help="Internal flag used by the dashboard", | ||||||
|  |                              action='store_true') | ||||||
|  |  | ||||||
|     return parser.parse_args(argv[1:]) |     return parser.parse_args(argv[1:]) | ||||||
|  |  | ||||||
|  |  | ||||||
| def run_esphomeyaml(argv): | def run_esphomeyaml(argv): | ||||||
|     args = parse_args(argv) |     args = parse_args(argv) | ||||||
|  |     if hasattr(args, 'dashboard'): | ||||||
|  |         core.FROM_DASHBOARD = args.dashboard | ||||||
|  |  | ||||||
|     setup_log(args.verbose) |     setup_log(args.verbose) | ||||||
|     if args.command in PRE_CONFIG_ACTIONS: |     if args.command in PRE_CONFIG_ACTIONS: | ||||||
|         try: |         try: | ||||||
|   | |||||||
| @@ -59,7 +59,7 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend({ | |||||||
| def validate(config): | def validate(config): | ||||||
|     if CONF_PASSWORD in config and CONF_SSID not in config: |     if CONF_PASSWORD in config and CONF_SSID not in config: | ||||||
|         raise vol.Invalid("Cannot have WiFi password without SSID!") |         raise vol.Invalid("Cannot have WiFi password without SSID!") | ||||||
|     if CONF_SSID not in config and CONF_AP not in config: |     if (CONF_SSID not in config) and (CONF_AP not in config): | ||||||
|         raise vol.Invalid("Please specify at least an SSID or an Access Point " |         raise vol.Invalid("Please specify at least an SSID or an Access Point " | ||||||
|                           "to create.") |                           "to create.") | ||||||
|     return config |     return config | ||||||
|   | |||||||
| @@ -2,10 +2,10 @@ from __future__ import print_function | |||||||
|  |  | ||||||
| from collections import OrderedDict | from collections import OrderedDict | ||||||
| import importlib | import importlib | ||||||
|  | import json | ||||||
| import logging | import logging | ||||||
|  |  | ||||||
| import voluptuous as vol | import voluptuous as vol | ||||||
| from voluptuous.humanize import humanize_error |  | ||||||
|  |  | ||||||
| from esphomeyaml import core, core_config, yaml_util | from esphomeyaml import core, core_config, yaml_util | ||||||
| from esphomeyaml.const import CONF_ESPHOMEYAML, CONF_PLATFORM, CONF_WIFI, ESP_PLATFORMS | from esphomeyaml.const import CONF_ESPHOMEYAML, CONF_PLATFORM, CONF_WIFI, ESP_PLATFORMS | ||||||
| @@ -268,22 +268,53 @@ def validate_config(config): | |||||||
| REQUIRED = ['esphomeyaml', 'wifi'] | REQUIRED = ['esphomeyaml', 'wifi'] | ||||||
|  |  | ||||||
|  |  | ||||||
| def _format_config_error(ex, domain, config): | def _nested_getitem(data, path): | ||||||
|     message = u"Invalid config for [{}]: ".format(domain) |     for item_index in path: | ||||||
|  |         try: | ||||||
|  |             data = data[item_index] | ||||||
|  |         except (KeyError, IndexError, TypeError): | ||||||
|  |             return None | ||||||
|  |     return data | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _format_path(path): | ||||||
|  |     return u'->'.join(unicode(m) for m in path) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def humanize_error(config, validation_error): | ||||||
|  |     offending_item_summary = _nested_getitem(config, validation_error.path) | ||||||
|  |     if isinstance(offending_item_summary, dict): | ||||||
|  |         offending_item_summary = json.dumps(offending_item_summary) | ||||||
|  |     return u'{}. Got {}'.format(validation_error, offending_item_summary) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _format_config_error(ex, domain, config, recursion=False): | ||||||
|  |     message = u"" if recursion else u"Invalid config for [{}]: ".format(domain) | ||||||
|  |     if isinstance(ex, vol.MultipleInvalid): | ||||||
|  |         return color('red', message + u'\n'.join(sorted( | ||||||
|  |             _format_config_error(sub_error, domain, config, recursion=True) | ||||||
|  |             for sub_error in ex.errors | ||||||
|  |         ))) | ||||||
|  |  | ||||||
|     if u'extra keys not allowed' in ex.error_message: |     if u'extra keys not allowed' in ex.error_message: | ||||||
|         message += u'[{}] is an invalid option for [{}]. Check: {}->{}.' \ |         message += u'[{}] is an invalid option for [{}].' \ | ||||||
|             .format(ex.path[-1], domain, domain, |             .format(ex.path[-1], domain) | ||||||
|                     u'->'.join(str(m) for m in ex.path)) |     elif u'required key not provided' in ex.error_message: | ||||||
|  |         message += u"'{}' is a required option for [{}]." \ | ||||||
|  |                    u"".format(ex.path[-1], domain) | ||||||
|     else: |     else: | ||||||
|         message += u'{}.'.format(humanize_error(config, ex)) |         message += u'{}.'.format(humanize_error(config, ex)) | ||||||
|  |  | ||||||
|  |     message += u' Check {}->{}.'.format(domain, _format_path(ex.path)) | ||||||
|  |     message = color('red', message) | ||||||
|  |  | ||||||
|     if isinstance(config, list): |     if isinstance(config, list): | ||||||
|         return message |         return message | ||||||
|  |  | ||||||
|     domain_config = config.get(domain, config) |     domain_config = config.get(domain, config) | ||||||
|     message += u" (See {}, line {}). ".format( |     message += color('cyan', u" (See {}, line {}). ".format( | ||||||
|         getattr(domain_config, '__config_file__', '?'), |         getattr(domain_config, '__config_file__', '?'), | ||||||
|         getattr(domain_config, '__line__', '?')) |         getattr(domain_config, '__line__', '?'))) | ||||||
|  |  | ||||||
|     return message |     return message | ||||||
|  |  | ||||||
| @@ -316,7 +347,7 @@ def line_info(obj, **kwargs): | |||||||
|     return '?' |     return '?' | ||||||
|  |  | ||||||
|  |  | ||||||
| def dump_dict(layer, indent_count=3, listi=False, **kwargs): | def dump_dict(layer, indent_count=0, listi=False, **kwargs): | ||||||
|     def sort_dict_key(val): |     def sort_dict_key(val): | ||||||
|         """Return the dict key for sorting.""" |         """Return the dict key for sorting.""" | ||||||
|         key = str.lower(val[0]) |         key = str.lower(val[0]) | ||||||
| @@ -358,9 +389,7 @@ def read_config(): | |||||||
|     if excepts: |     if excepts: | ||||||
|         safe_print(color('bold_white', u"Failed config")) |         safe_print(color('bold_white', u"Failed config")) | ||||||
|         for domain, config in excepts.iteritems(): |         for domain, config in excepts.iteritems(): | ||||||
|             safe_print(u' {} {}'.format(color('bold_red', domain + u':'), |             safe_print(color('bold_red', domain + u':')) | ||||||
|                                         color('red', '', reset='red'))) |             dump_dict(config) | ||||||
|             dump_dict(config, reset='red') |  | ||||||
|             safe_print(color('reset')) |  | ||||||
|         return None |         return None | ||||||
|     return OrderedDict(res) |     return OrderedDict(res) | ||||||
|   | |||||||
| @@ -398,3 +398,4 @@ CORE = EsphomeyamlCore() | |||||||
|  |  | ||||||
| ConfigType = Dict[str, Any] | ConfigType = Dict[str, Any] | ||||||
| CoreType = EsphomeyamlCore | CoreType = EsphomeyamlCore | ||||||
|  | FROM_DASHBOARD = False | ||||||
|   | |||||||
| @@ -105,7 +105,7 @@ class EsphomeyamlLogsHandler(EsphomeyamlCommandWebSocket): | |||||||
|     def build_command(self, message): |     def build_command(self, message): | ||||||
|         js = json.loads(message) |         js = json.loads(message) | ||||||
|         config_file = CONFIG_DIR + '/' + js['configuration'] |         config_file = CONFIG_DIR + '/' + js['configuration'] | ||||||
|         return ["esphomeyaml", config_file, "logs", '--serial-port', js["port"], '--escape'] |         return ["esphomeyaml", config_file, "logs", '--serial-port', js["port"], '--dashboard'] | ||||||
|  |  | ||||||
|  |  | ||||||
| class EsphomeyamlRunHandler(EsphomeyamlCommandWebSocket): | class EsphomeyamlRunHandler(EsphomeyamlCommandWebSocket): | ||||||
| @@ -113,42 +113,42 @@ class EsphomeyamlRunHandler(EsphomeyamlCommandWebSocket): | |||||||
|         js = json.loads(message) |         js = json.loads(message) | ||||||
|         config_file = os.path.join(CONFIG_DIR, js['configuration']) |         config_file = os.path.join(CONFIG_DIR, js['configuration']) | ||||||
|         return ["esphomeyaml", config_file, "run", '--upload-port', js["port"], |         return ["esphomeyaml", config_file, "run", '--upload-port', js["port"], | ||||||
|                 '--escape', '--use-esptoolpy'] |                 '--dashboard'] | ||||||
|  |  | ||||||
|  |  | ||||||
| class EsphomeyamlCompileHandler(EsphomeyamlCommandWebSocket): | class EsphomeyamlCompileHandler(EsphomeyamlCommandWebSocket): | ||||||
|     def build_command(self, message): |     def build_command(self, message): | ||||||
|         js = json.loads(message) |         js = json.loads(message) | ||||||
|         config_file = os.path.join(CONFIG_DIR, js['configuration']) |         config_file = os.path.join(CONFIG_DIR, js['configuration']) | ||||||
|         return ["esphomeyaml", config_file, "compile"] |         return ["esphomeyaml", config_file, "compile", '--dashboard'] | ||||||
|  |  | ||||||
|  |  | ||||||
| class EsphomeyamlValidateHandler(EsphomeyamlCommandWebSocket): | class EsphomeyamlValidateHandler(EsphomeyamlCommandWebSocket): | ||||||
|     def build_command(self, message): |     def build_command(self, message): | ||||||
|         js = json.loads(message) |         js = json.loads(message) | ||||||
|         config_file = os.path.join(CONFIG_DIR, js['configuration']) |         config_file = os.path.join(CONFIG_DIR, js['configuration']) | ||||||
|         return ["esphomeyaml", config_file, "config"] |         return ["esphomeyaml", config_file, "config", '--dashboard'] | ||||||
|  |  | ||||||
|  |  | ||||||
| class EsphomeyamlCleanMqttHandler(EsphomeyamlCommandWebSocket): | class EsphomeyamlCleanMqttHandler(EsphomeyamlCommandWebSocket): | ||||||
|     def build_command(self, message): |     def build_command(self, message): | ||||||
|         js = json.loads(message) |         js = json.loads(message) | ||||||
|         config_file = os.path.join(CONFIG_DIR, js['configuration']) |         config_file = os.path.join(CONFIG_DIR, js['configuration']) | ||||||
|         return ["esphomeyaml", config_file, "clean-mqtt"] |         return ["esphomeyaml", config_file, "clean-mqtt", '--dashboard'] | ||||||
|  |  | ||||||
|  |  | ||||||
| class EsphomeyamlCleanHandler(EsphomeyamlCommandWebSocket): | class EsphomeyamlCleanHandler(EsphomeyamlCommandWebSocket): | ||||||
|     def build_command(self, message): |     def build_command(self, message): | ||||||
|         js = json.loads(message) |         js = json.loads(message) | ||||||
|         config_file = os.path.join(CONFIG_DIR, js['configuration']) |         config_file = os.path.join(CONFIG_DIR, js['configuration']) | ||||||
|         return ["esphomeyaml", config_file, "clean"] |         return ["esphomeyaml", config_file, "clean", '--dashboard'] | ||||||
|  |  | ||||||
|  |  | ||||||
| class EsphomeyamlHassConfigHandler(EsphomeyamlCommandWebSocket): | class EsphomeyamlHassConfigHandler(EsphomeyamlCommandWebSocket): | ||||||
|     def build_command(self, message): |     def build_command(self, message): | ||||||
|         js = json.loads(message) |         js = json.loads(message) | ||||||
|         config_file = os.path.join(CONFIG_DIR, js['configuration']) |         config_file = os.path.join(CONFIG_DIR, js['configuration']) | ||||||
|         return ["esphomeyaml", config_file, "hass-config"] |         return ["esphomeyaml", config_file, "hass-config", '--dashboard'] | ||||||
|  |  | ||||||
|  |  | ||||||
| class SerialPortRequestHandler(BaseHandler): | class SerialPortRequestHandler(BaseHandler): | ||||||
|   | |||||||
| @@ -5,19 +5,19 @@ document.addEventListener('DOMContentLoaded', () => { | |||||||
| const colorReplace = (input) => { | const colorReplace = (input) => { | ||||||
| input = input.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"); | 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\[(?:0;)?31m/g, '<span class="e">'); | ||||||
| input = input.replace(/\\033\[(?:1;)?31m/g, '<span class="e bold">'); | input = input.replace(/\\033\[(?:0?1;)?31m/g, '<span class="e bold">'); | ||||||
| input = input.replace(/\\033\[(?:0;)?32m/g, '<span class="i">'); | 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?1;)?32m/g, '<span class="i bold">'); | ||||||
| input = input.replace(/\\033\[(?:0;)?33m/g, '<span class="w">'); | 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?1;)?33m/g, '<span class="w bold">'); | ||||||
| input = input.replace(/\\033\[(?:0;)?35m/g, '<span class="c">'); | 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?1;)?35m/g, '<span class="c bold">'); | ||||||
| input = input.replace(/\\033\[(?:0;)?36m/g, '<span class="d">'); | 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?1;)?36m/g, '<span class="d bold">'); | ||||||
| input = input.replace(/\\033\[(?:0;)?37m/g, '<span class="v">'); | 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?1;)?37m/g, '<span class="v bold">'); | ||||||
| input = input.replace(/\\033\[(?:0;)?38m/g, '<span class="vv">'); | 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\[(?:0?1;)?38m/g, '<span class="vv bold">'); | ||||||
| input = input.replace(/\\033\[0m/g, '</span>'); | input = input.replace(/\\033\[0m/g, '</span>'); | ||||||
|  |  | ||||||
| return input; | return input; | ||||||
| @@ -26,50 +26,50 @@ return input; | |||||||
| let configuration = ""; | let configuration = ""; | ||||||
| let wsProtocol = "ws:"; | let wsProtocol = "ws:"; | ||||||
| if (window.location.protocol === "https:") { | if (window.location.protocol === "https:") { | ||||||
| wsProtocol = 'wss:'; |   wsProtocol = 'wss:'; | ||||||
| } | } | ||||||
| const wsUrl = wsProtocol + '//' + window.location.hostname + ':' + window.location.port; | const wsUrl = wsProtocol + '//' + window.location.hostname + ':' + window.location.port; | ||||||
|  |  | ||||||
| let isFetchingPing = false; | let isFetchingPing = false; | ||||||
| const fetchPing = () => { | const fetchPing = () => { | ||||||
| if (isFetchingPing) |   if (isFetchingPing) | ||||||
|     return; |       return; | ||||||
| isFetchingPing = true; |   isFetchingPing = true; | ||||||
|  |  | ||||||
| fetch('/ping', {credentials: "same-origin"}).then(res => res.json()) |   fetch('/ping', {credentials: "same-origin"}).then(res => res.json()) | ||||||
|   .then(response => { |     .then(response => { | ||||||
|     for (let filename in response) { |       for (let filename in response) { | ||||||
|       let node = document.querySelector(`.status-indicator[data-node="${filename}"]`); |         let node = document.querySelector(`.status-indicator[data-node="${filename}"]`); | ||||||
|       if (node === null) |         if (node === null) | ||||||
|         continue; |           continue; | ||||||
|  |  | ||||||
|       let status = response[filename]; |         let status = response[filename]; | ||||||
|       let klass; |         let klass; | ||||||
|       if (status === null) { |         if (status === null) { | ||||||
|         klass = 'unknown'; |           klass = 'unknown'; | ||||||
|       } else if (status === true) { |         } else if (status === true) { | ||||||
|         klass = 'online'; |           klass = 'online'; | ||||||
|         node.setAttribute('data-last-connected', Date.now().toString()); |           node.setAttribute('data-last-connected', Date.now().toString()); | ||||||
|       } else if (node.hasAttribute('data-last-connected')) { |         } else if (node.hasAttribute('data-last-connected')) { | ||||||
|         const attr = parseInt(node.getAttribute('data-last-connected')); |           const attr = parseInt(node.getAttribute('data-last-connected')); | ||||||
|         if (Date.now() - attr <= 5000) { |           if (Date.now() - attr <= 5000) { | ||||||
|           klass = 'not-responding'; |             klass = 'not-responding'; | ||||||
|  |           } else { | ||||||
|  |             klass = 'offline'; | ||||||
|  |           } | ||||||
|         } else { |         } else { | ||||||
|           klass = 'offline'; |           klass = 'offline'; | ||||||
|         } |         } | ||||||
|       } else { |  | ||||||
|         klass = 'offline'; |         if (node.classList.contains(klass)) | ||||||
|  |           continue; | ||||||
|  |  | ||||||
|  |         node.classList.remove('unknown', 'online', 'offline', 'not-responding'); | ||||||
|  |         node.classList.add(klass); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       if (node.classList.contains(klass)) |       isFetchingPing = false; | ||||||
|         continue; |     }); | ||||||
|  |  | ||||||
|       node.classList.remove('unknown', 'online', 'offline', 'not-responding'); |  | ||||||
|       node.classList.add(klass); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     isFetchingPing = false; |  | ||||||
|   }); |  | ||||||
| }; | }; | ||||||
| setInterval(fetchPing, 2000); | setInterval(fetchPing, 2000); | ||||||
| fetchPing(); | fetchPing(); | ||||||
| @@ -78,53 +78,53 @@ const portSelect = document.querySelector('.nav-wrapper select'); | |||||||
| let ports = []; | let ports = []; | ||||||
|  |  | ||||||
| const fetchSerialPorts = (begin=false) => { | const fetchSerialPorts = (begin=false) => { | ||||||
| fetch('/serial-ports', {credentials: "same-origin"}).then(res => res.json()) |   fetch('/serial-ports', {credentials: "same-origin"}).then(res => res.json()) | ||||||
|   .then(response => { |     .then(response => { | ||||||
|     if (ports.length === response.length) { |       if (ports.length === response.length) { | ||||||
|       let allEqual = true; |         let allEqual = true; | ||||||
|  |         for (let i = 0; i < response.length; i++) { | ||||||
|  |           if (ports[i].port !== response[i].port) { | ||||||
|  |             allEqual = false; | ||||||
|  |             break; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         if (allEqual) | ||||||
|  |           return; | ||||||
|  |       } | ||||||
|  |       const hasNewPort = response.length >= ports.length; | ||||||
|  |  | ||||||
|  |       ports = response; | ||||||
|  |  | ||||||
|  |       const inst = M.FormSelect.getInstance(portSelect); | ||||||
|  |       if (inst !== undefined) { | ||||||
|  |         inst.destroy(); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       portSelect.innerHTML = ""; | ||||||
|  |       const prevSelected = getUploadPort(); | ||||||
|       for (let i = 0; i < response.length; i++) { |       for (let i = 0; i < response.length; i++) { | ||||||
|         if (ports[i].port !== response[i].port) { |         const val = response[i]; | ||||||
|           allEqual = false; |         if (val.port === prevSelected) { | ||||||
|           break; |           portSelect.innerHTML += `<option value="${val.port}" selected>${val.port} (${val.desc})</option>`; | ||||||
|  |         } else { | ||||||
|  |           portSelect.innerHTML += `<option value="${val.port}">${val.port} (${val.desc})</option>`; | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       if (allEqual) |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|     const hasNewPort = response.length >= ports.length; |  | ||||||
|  |  | ||||||
|     ports = response; |       M.FormSelect.init(portSelect, {}); | ||||||
|  |       if (!begin && hasNewPort) | ||||||
|     const inst = M.FormSelect.getInstance(portSelect); |         M.toast({html: "Discovered new serial port."}); | ||||||
|     if (inst !== undefined) { |     }); | ||||||
|       inst.destroy(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     portSelect.innerHTML = ""; |  | ||||||
|     const prevSelected = getUploadPort(); |  | ||||||
|     for (let i = 0; i < response.length; i++) { |  | ||||||
|       const val = response[i]; |  | ||||||
|       if (val.port === prevSelected) { |  | ||||||
|         portSelect.innerHTML += `<option value="${val.port}" selected>${val.port} (${val.desc})</option>`; |  | ||||||
|       } else { |  | ||||||
|         portSelect.innerHTML += `<option value="${val.port}">${val.port} (${val.desc})</option>`; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     M.FormSelect.init(portSelect, {}); |  | ||||||
|     if (!begin && hasNewPort) |  | ||||||
|       M.toast({html: "Discovered new serial port."}); |  | ||||||
|   }); |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const getUploadPort = () => { | const getUploadPort = () => { | ||||||
| const inst = M.FormSelect.getInstance(portSelect); |   const inst = M.FormSelect.getInstance(portSelect); | ||||||
| if (inst === undefined) { |   if (inst === undefined) { | ||||||
|   return "OTA"; |     return "OTA"; | ||||||
| } |   } | ||||||
|  |  | ||||||
| inst._setSelectedStates(); |   inst._setSelectedStates(); | ||||||
| return inst.getSelectedValues()[0]; |   return inst.getSelectedValues()[0]; | ||||||
| }; | }; | ||||||
| setInterval(fetchSerialPorts, 5000); | setInterval(fetchSerialPorts, 5000); | ||||||
| fetchSerialPorts(true); | fetchSerialPorts(true); | ||||||
| @@ -132,347 +132,348 @@ fetchSerialPorts(true); | |||||||
| const logsModalElem = document.getElementById("modal-logs"); | const logsModalElem = document.getElementById("modal-logs"); | ||||||
|  |  | ||||||
| document.querySelectorAll(".action-show-logs").forEach((showLogs) => { | document.querySelectorAll(".action-show-logs").forEach((showLogs) => { | ||||||
| showLogs.addEventListener('click', (e) => { |   showLogs.addEventListener('click', (e) => { | ||||||
|   configuration = e.target.getAttribute('data-node'); |     configuration = e.target.getAttribute('data-node'); | ||||||
|   const modalInstance = M.Modal.getInstance(logsModalElem); |     const modalInstance = M.Modal.getInstance(logsModalElem); | ||||||
|   const log = logsModalElem.querySelector(".log"); |     const log = logsModalElem.querySelector(".log"); | ||||||
|   log.innerHTML = ""; |     log.innerHTML = ""; | ||||||
|   const stopLogsButton = logsModalElem.querySelector(".stop-logs"); |     const stopLogsButton = logsModalElem.querySelector(".stop-logs"); | ||||||
|   let stopped = false; |     let stopped = false; | ||||||
|   stopLogsButton.innerHTML = "Stop"; |     stopLogsButton.innerHTML = "Stop"; | ||||||
|   modalInstance.open(); |     modalInstance.open(); | ||||||
|  |  | ||||||
|   const filenameField = logsModalElem.querySelector('.filename'); |     const filenameField = logsModalElem.querySelector('.filename'); | ||||||
|   filenameField.innerHTML = configuration; |     filenameField.innerHTML = configuration; | ||||||
|  |  | ||||||
|   const logSocket = new WebSocket(wsUrl + "/logs"); |     const logSocket = new WebSocket(wsUrl + "/logs"); | ||||||
|   logSocket.addEventListener('message', (event) => { |     logSocket.addEventListener('message', (event) => { | ||||||
|     const data = JSON.parse(event.data); |       const data = JSON.parse(event.data); | ||||||
|     if (data.event === "line") { |       if (data.event === "line") { | ||||||
|       const msg = data.data; |         const msg = data.data; | ||||||
|       log.innerHTML += colorReplace(msg); |         log.innerHTML += colorReplace(msg); | ||||||
|     } else if (data.event === "exit") { |       } else if (data.event === "exit") { | ||||||
|       if (data.code === 0) { |         if (data.code === 0) { | ||||||
|         M.toast({html: "Program exited successfully."}); |           M.toast({html: "Program exited successfully."}); | ||||||
|       } else { |         } else { | ||||||
|         M.toast({html: `Program failed with code ${data.code}`}); |           M.toast({html: `Program failed with code ${data.code}`}); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         stopLogsButton.innerHTML = "Close"; | ||||||
|  |         stopped = true; | ||||||
|       } |       } | ||||||
|  |     }); | ||||||
|       stopLogsButton.innerHTML = "Close"; |     logSocket.addEventListener('open', () => { | ||||||
|       stopped = true; |       const msg = JSON.stringify({configuration: configuration, port: getUploadPort()}); | ||||||
|     } |       logSocket.send(msg); | ||||||
|  |     }); | ||||||
|  |     logSocket.addEventListener('close', () => { | ||||||
|  |       if (!stopped) { | ||||||
|  |         M.toast({html: 'Terminated process.'}); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |     modalInstance.options.onCloseStart = () => { | ||||||
|  |       logSocket.close(); | ||||||
|  |     }; | ||||||
|   }); |   }); | ||||||
|   logSocket.addEventListener('open', () => { |  | ||||||
|     const msg = JSON.stringify({configuration: configuration, port: getUploadPort()}); |  | ||||||
|     logSocket.send(msg); |  | ||||||
|   }); |  | ||||||
|   logSocket.addEventListener('close', () => { |  | ||||||
|     if (!stopped) { |  | ||||||
|       M.toast({html: 'Terminated process.'}); |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
|   modalInstance.options.onCloseStart = () => { |  | ||||||
|     logSocket.close(); |  | ||||||
|   }; |  | ||||||
| }); |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
| const uploadModalElem = document.getElementById("modal-upload"); | const uploadModalElem = document.getElementById("modal-upload"); | ||||||
|  |  | ||||||
| document.querySelectorAll(".action-upload").forEach((upload) => { | document.querySelectorAll(".action-upload").forEach((upload) => { | ||||||
| upload.addEventListener('click', (e) => { |   upload.addEventListener('click', (e) => { | ||||||
|   configuration = e.target.getAttribute('data-node'); |     configuration = e.target.getAttribute('data-node'); | ||||||
|   const modalInstance = M.Modal.getInstance(uploadModalElem); |     const modalInstance = M.Modal.getInstance(uploadModalElem); | ||||||
|   const log = uploadModalElem.querySelector(".log"); |     const log = uploadModalElem.querySelector(".log"); | ||||||
|   log.innerHTML = ""; |     log.innerHTML = ""; | ||||||
|   const stopLogsButton = uploadModalElem.querySelector(".stop-logs"); |     const stopLogsButton = uploadModalElem.querySelector(".stop-logs"); | ||||||
|   let stopped = false; |     let stopped = false; | ||||||
|   stopLogsButton.innerHTML = "Stop"; |     stopLogsButton.innerHTML = "Stop"; | ||||||
|   modalInstance.open(); |     modalInstance.open(); | ||||||
|  |  | ||||||
|   const filenameField = uploadModalElem.querySelector('.filename'); |     const filenameField = uploadModalElem.querySelector('.filename'); | ||||||
|   filenameField.innerHTML = configuration; |     filenameField.innerHTML = configuration; | ||||||
|  |  | ||||||
|   const logSocket = new WebSocket(wsUrl + "/run"); |     const logSocket = new WebSocket(wsUrl + "/run"); | ||||||
|   logSocket.addEventListener('message', (event) => { |     logSocket.addEventListener('message', (event) => { | ||||||
|     const data = JSON.parse(event.data); |       const data = JSON.parse(event.data); | ||||||
|     if (data.event === "line") { |       if (data.event === "line") { | ||||||
|       const msg = data.data; |         const msg = data.data; | ||||||
|       log.innerHTML += colorReplace(msg); |         log.innerHTML += colorReplace(msg); | ||||||
|     } else if (data.event === "exit") { |       } else if (data.event === "exit") { | ||||||
|       if (data.code === 0) { |         if (data.code === 0) { | ||||||
|         M.toast({html: "Program exited successfully."}); |           M.toast({html: "Program exited successfully."}); | ||||||
|       } else { |         } else { | ||||||
|         M.toast({html: `Program failed with code ${data.code}`}); |           M.toast({html: `Program failed with code ${data.code}`}); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         stopLogsButton.innerHTML = "Close"; | ||||||
|  |         stopped = true; | ||||||
|       } |       } | ||||||
|  |     }); | ||||||
|       stopLogsButton.innerHTML = "Close"; |     logSocket.addEventListener('open', () => { | ||||||
|       stopped = true; |       const msg = JSON.stringify({configuration: configuration, port: getUploadPort()}); | ||||||
|     } |       logSocket.send(msg); | ||||||
|  |     }); | ||||||
|  |     logSocket.addEventListener('close', () => { | ||||||
|  |       if (!stopped) { | ||||||
|  |         M.toast({html: 'Terminated process.'}); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |     modalInstance.options.onCloseStart = () => { | ||||||
|  |       logSocket.close(); | ||||||
|  |     }; | ||||||
|   }); |   }); | ||||||
|   logSocket.addEventListener('open', () => { |  | ||||||
|     const msg = JSON.stringify({configuration: configuration, port: getUploadPort()}); |  | ||||||
|     logSocket.send(msg); |  | ||||||
|   }); |  | ||||||
|   logSocket.addEventListener('close', () => { |  | ||||||
|     if (!stopped) { |  | ||||||
|       M.toast({html: 'Terminated process.'}); |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
|   modalInstance.options.onCloseStart = () => { |  | ||||||
|     logSocket.close(); |  | ||||||
|   }; |  | ||||||
| }); |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
| const validateModalElem = document.getElementById("modal-validate"); | const validateModalElem = document.getElementById("modal-validate"); | ||||||
|  |  | ||||||
| document.querySelectorAll(".action-validate").forEach((upload) => { | document.querySelectorAll(".action-validate").forEach((upload) => { | ||||||
| upload.addEventListener('click', (e) => { |   upload.addEventListener('click', (e) => { | ||||||
|   configuration = e.target.getAttribute('data-node'); |     configuration = e.target.getAttribute('data-node'); | ||||||
|   const modalInstance = M.Modal.getInstance(validateModalElem); |     const modalInstance = M.Modal.getInstance(validateModalElem); | ||||||
|   const log = validateModalElem.querySelector(".log"); |     const log = validateModalElem.querySelector(".log"); | ||||||
|   log.innerHTML = ""; |     log.innerHTML = ""; | ||||||
|   const stopLogsButton = validateModalElem.querySelector(".stop-logs"); |     const stopLogsButton = validateModalElem.querySelector(".stop-logs"); | ||||||
|   let stopped = false; |     let stopped = false; | ||||||
|   stopLogsButton.innerHTML = "Stop"; |     stopLogsButton.innerHTML = "Stop"; | ||||||
|   modalInstance.open(); |     modalInstance.open(); | ||||||
|  |  | ||||||
|   const filenameField = validateModalElem.querySelector('.filename'); |     const filenameField = validateModalElem.querySelector('.filename'); | ||||||
|   filenameField.innerHTML = configuration; |     filenameField.innerHTML = configuration; | ||||||
|  |  | ||||||
|   const logSocket = new WebSocket(wsUrl + "/validate"); |     const logSocket = new WebSocket(wsUrl + "/validate"); | ||||||
|   logSocket.addEventListener('message', (event) => { |     logSocket.addEventListener('message', (event) => { | ||||||
|     const data = JSON.parse(event.data); |       const data = JSON.parse(event.data); | ||||||
|     if (data.event === "line") { |       if (data.event === "line") { | ||||||
|       const msg = data.data; |         const msg = data.data; | ||||||
|       log.innerHTML += colorReplace(msg); |         log.innerHTML += colorReplace(msg); | ||||||
|     } else if (data.event === "exit") { |       } else if (data.event === "exit") { | ||||||
|       if (data.code === 0) { |         if (data.code === 0) { | ||||||
|         M.toast({ |           M.toast({ | ||||||
|           html: `<code class="inlinecode">${configuration}</code> is valid 👍`, |             html: `<code class="inlinecode">${configuration}</code> is valid 👍`, | ||||||
|           displayLength: 5000, |             displayLength: 5000, | ||||||
|         }); |           }); | ||||||
|       } else { |         } else { | ||||||
|         M.toast({ |           M.toast({ | ||||||
|           html: `<code class="inlinecode">${configuration}</code> is invalid 😕`, |             html: `<code class="inlinecode">${configuration}</code> is invalid 😕`, | ||||||
|           displayLength: 5000, |             displayLength: 5000, | ||||||
|         }); |           }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         stopLogsButton.innerHTML = "Close"; | ||||||
|  |         stopped = true; | ||||||
|       } |       } | ||||||
|  |     }); | ||||||
|       stopLogsButton.innerHTML = "Close"; |     logSocket.addEventListener('open', () => { | ||||||
|       stopped = true; |       const msg = JSON.stringify({configuration: configuration}); | ||||||
|     } |       logSocket.send(msg); | ||||||
|  |     }); | ||||||
|  |     logSocket.addEventListener('close', () => { | ||||||
|  |       if (!stopped) { | ||||||
|  |         M.toast({html: 'Terminated process.'}); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |     modalInstance.options.onCloseStart = () => { | ||||||
|  |       logSocket.close(); | ||||||
|  |     }; | ||||||
|   }); |   }); | ||||||
|   logSocket.addEventListener('open', () => { |  | ||||||
|     const msg = JSON.stringify({configuration: configuration}); |  | ||||||
|     logSocket.send(msg); |  | ||||||
|   }); |  | ||||||
|   logSocket.addEventListener('close', () => { |  | ||||||
|     if (!stopped) { |  | ||||||
|       M.toast({html: 'Terminated process.'}); |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
|   modalInstance.options.onCloseStart = () => { |  | ||||||
|     logSocket.close(); |  | ||||||
|   }; |  | ||||||
| }); |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
| const compileModalElem = document.getElementById("modal-compile"); | const compileModalElem = document.getElementById("modal-compile"); | ||||||
| const downloadButton = compileModalElem.querySelector('.download-binary'); | const downloadButton = compileModalElem.querySelector('.download-binary'); | ||||||
|  |  | ||||||
| document.querySelectorAll(".action-compile").forEach((upload) => { | document.querySelectorAll(".action-compile").forEach((upload) => { | ||||||
| upload.addEventListener('click', (e) => { |   upload.addEventListener('click', (e) => { | ||||||
|   configuration = e.target.getAttribute('data-node'); |     configuration = e.target.getAttribute('data-node'); | ||||||
|   const modalInstance = M.Modal.getInstance(compileModalElem); |     const modalInstance = M.Modal.getInstance(compileModalElem); | ||||||
|   const log = compileModalElem.querySelector(".log"); |     const log = compileModalElem.querySelector(".log"); | ||||||
|   log.innerHTML = ""; |     log.innerHTML = ""; | ||||||
|   const stopLogsButton = compileModalElem.querySelector(".stop-logs"); |     const stopLogsButton = compileModalElem.querySelector(".stop-logs"); | ||||||
|   let stopped = false; |     let stopped = false; | ||||||
|   stopLogsButton.innerHTML = "Stop"; |     stopLogsButton.innerHTML = "Stop"; | ||||||
|   downloadButton.classList.add('disabled'); |     downloadButton.classList.add('disabled'); | ||||||
|  |  | ||||||
|   modalInstance.open(); |     modalInstance.open(); | ||||||
|  |  | ||||||
|   const filenameField = compileModalElem.querySelector('.filename'); |     const filenameField = compileModalElem.querySelector('.filename'); | ||||||
|   filenameField.innerHTML = configuration; |     filenameField.innerHTML = configuration; | ||||||
|  |  | ||||||
|   const logSocket = new WebSocket(wsUrl + "/compile"); |     const logSocket = new WebSocket(wsUrl + "/compile"); | ||||||
|   logSocket.addEventListener('message', (event) => { |     logSocket.addEventListener('message', (event) => { | ||||||
|     const data = JSON.parse(event.data); |       const data = JSON.parse(event.data); | ||||||
|     if (data.event === "line") { |       if (data.event === "line") { | ||||||
|       const msg = data.data; |         const msg = data.data; | ||||||
|       log.innerHTML += colorReplace(msg); |         log.innerHTML += colorReplace(msg); | ||||||
|     } else if (data.event === "exit") { |       } else if (data.event === "exit") { | ||||||
|       if (data.code === 0) { |         if (data.code === 0) { | ||||||
|         M.toast({html: "Program exited successfully."}); |           M.toast({html: "Program exited successfully."}); | ||||||
|         downloadButton.classList.remove('disabled'); |           downloadButton.classList.remove('disabled'); | ||||||
|       } else { |         } else { | ||||||
|         M.toast({html: `Program failed with code ${data.code}`}); |           M.toast({html: `Program failed with code ${data.code}`}); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         stopLogsButton.innerHTML = "Close"; | ||||||
|  |         stopped = true; | ||||||
|       } |       } | ||||||
|  |     }); | ||||||
|  |     logSocket.addEventListener('open', () => { | ||||||
|  |       const msg = JSON.stringify({configuration: configuration}); | ||||||
|  |       logSocket.send(msg); | ||||||
|  |     }); | ||||||
|  |     logSocket.addEventListener('close', () => { | ||||||
|  |       if (!stopped) { | ||||||
|  |         M.toast({html: 'Terminated process.'}); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |     modalInstance.options.onCloseStart = () => { | ||||||
|  |       logSocket.close(); | ||||||
|  |     }; | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  |  | ||||||
|       stopLogsButton.innerHTML = "Close"; |  | ||||||
|       stopped = true; |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
|   logSocket.addEventListener('open', () => { |  | ||||||
|     const msg = JSON.stringify({configuration: configuration}); |  | ||||||
|     logSocket.send(msg); |  | ||||||
|   }); |  | ||||||
|   logSocket.addEventListener('close', () => { |  | ||||||
|     if (!stopped) { |  | ||||||
|       M.toast({html: 'Terminated process.'}); |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
|   modalInstance.options.onCloseStart = () => { |  | ||||||
|     logSocket.close(); |  | ||||||
|   }; |  | ||||||
| }); |  | ||||||
| }); |  | ||||||
| downloadButton.addEventListener('click', () => { | downloadButton.addEventListener('click', () => { | ||||||
| const link = document.createElement("a"); |   const link = document.createElement("a"); | ||||||
| link.download = name; |   link.download = name; | ||||||
| link.href = '/download.bin?configuration=' + encodeURIComponent(configuration); |   link.href = '/download.bin?configuration=' + encodeURIComponent(configuration); | ||||||
| link.click(); |   link.click(); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| const cleanMqttModalElem = document.getElementById("modal-clean-mqtt"); | const cleanMqttModalElem = document.getElementById("modal-clean-mqtt"); | ||||||
|  |  | ||||||
| document.querySelectorAll(".action-clean-mqtt").forEach((btn) => { | document.querySelectorAll(".action-clean-mqtt").forEach((btn) => { | ||||||
| btn.addEventListener('click', (e) => { |   btn.addEventListener('click', (e) => { | ||||||
|   configuration = e.target.getAttribute('data-node'); |     configuration = e.target.getAttribute('data-node'); | ||||||
|   const modalInstance = M.Modal.getInstance(cleanMqttModalElem); |     const modalInstance = M.Modal.getInstance(cleanMqttModalElem); | ||||||
|   const log = cleanMqttModalElem.querySelector(".log"); |     const log = cleanMqttModalElem.querySelector(".log"); | ||||||
|   log.innerHTML = ""; |     log.innerHTML = ""; | ||||||
|   const stopLogsButton = cleanMqttModalElem.querySelector(".stop-logs"); |     const stopLogsButton = cleanMqttModalElem.querySelector(".stop-logs"); | ||||||
|   let stopped = false; |     let stopped = false; | ||||||
|   stopLogsButton.innerHTML = "Stop"; |     stopLogsButton.innerHTML = "Stop"; | ||||||
|   modalInstance.open(); |     modalInstance.open(); | ||||||
|  |  | ||||||
|   const filenameField = cleanMqttModalElem.querySelector('.filename'); |     const filenameField = cleanMqttModalElem.querySelector('.filename'); | ||||||
|   filenameField.innerHTML = configuration; |     filenameField.innerHTML = configuration; | ||||||
|  |  | ||||||
|   const logSocket = new WebSocket(wsUrl + "/clean-mqtt"); |     const logSocket = new WebSocket(wsUrl + "/clean-mqtt"); | ||||||
|   logSocket.addEventListener('message', (event) => { |     logSocket.addEventListener('message', (event) => { | ||||||
|     const data = JSON.parse(event.data); |       const data = JSON.parse(event.data); | ||||||
|     if (data.event === "line") { |       if (data.event === "line") { | ||||||
|       const msg = data.data; |         const msg = data.data; | ||||||
|       log.innerHTML += colorReplace(msg); |         log.innerHTML += colorReplace(msg); | ||||||
|     } else if (data.event === "exit") { |       } else if (data.event === "exit") { | ||||||
|       stopLogsButton.innerHTML = "Close"; |         stopLogsButton.innerHTML = "Close"; | ||||||
|       stopped = true; |         stopped = true; | ||||||
|     } |       } | ||||||
|  |     }); | ||||||
|  |     logSocket.addEventListener('open', () => { | ||||||
|  |       const msg = JSON.stringify({configuration: configuration}); | ||||||
|  |       logSocket.send(msg); | ||||||
|  |     }); | ||||||
|  |     logSocket.addEventListener('close', () => { | ||||||
|  |       if (!stopped) { | ||||||
|  |         M.toast({html: 'Terminated process.'}); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |     modalInstance.options.onCloseStart = () => { | ||||||
|  |       logSocket.close(); | ||||||
|  |     }; | ||||||
|   }); |   }); | ||||||
|   logSocket.addEventListener('open', () => { |  | ||||||
|     const msg = JSON.stringify({configuration: configuration}); |  | ||||||
|     logSocket.send(msg); |  | ||||||
|   }); |  | ||||||
|   logSocket.addEventListener('close', () => { |  | ||||||
|     if (!stopped) { |  | ||||||
|       M.toast({html: 'Terminated process.'}); |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
|   modalInstance.options.onCloseStart = () => { |  | ||||||
|     logSocket.close(); |  | ||||||
|   }; |  | ||||||
| }); |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
| const cleanModalElem = document.getElementById("modal-clean"); | const cleanModalElem = document.getElementById("modal-clean"); | ||||||
|  |  | ||||||
| document.querySelectorAll(".action-clean").forEach((btn) => { | document.querySelectorAll(".action-clean").forEach((btn) => { | ||||||
| btn.addEventListener('click', (e) => { |   btn.addEventListener('click', (e) => { | ||||||
|   configuration = e.target.getAttribute('data-node'); |     configuration = e.target.getAttribute('data-node'); | ||||||
|   const modalInstance = M.Modal.getInstance(cleanModalElem); |     const modalInstance = M.Modal.getInstance(cleanModalElem); | ||||||
|   const log = cleanModalElem.querySelector(".log"); |     const log = cleanModalElem.querySelector(".log"); | ||||||
|   log.innerHTML = ""; |     log.innerHTML = ""; | ||||||
|   const stopLogsButton = cleanModalElem.querySelector(".stop-logs"); |     const stopLogsButton = cleanModalElem.querySelector(".stop-logs"); | ||||||
|   let stopped = false; |     let stopped = false; | ||||||
|   stopLogsButton.innerHTML = "Stop"; |     stopLogsButton.innerHTML = "Stop"; | ||||||
|   modalInstance.open(); |     modalInstance.open(); | ||||||
|  |  | ||||||
|   const filenameField = cleanModalElem.querySelector('.filename'); |     const filenameField = cleanModalElem.querySelector('.filename'); | ||||||
|   filenameField.innerHTML = configuration; |     filenameField.innerHTML = configuration; | ||||||
|  |  | ||||||
|   const logSocket = new WebSocket(wsUrl + "/clean"); |     const logSocket = new WebSocket(wsUrl + "/clean"); | ||||||
|   logSocket.addEventListener('message', (event) => { |     logSocket.addEventListener('message', (event) => { | ||||||
|     const data = JSON.parse(event.data); |       const data = JSON.parse(event.data); | ||||||
|     if (data.event === "line") { |       if (data.event === "line") { | ||||||
|       const msg = data.data; |         const msg = data.data; | ||||||
|       log.innerHTML += colorReplace(msg); |         log.innerHTML += colorReplace(msg); | ||||||
|     } else if (data.event === "exit") { |       } else if (data.event === "exit") { | ||||||
|       if (data.code === 0) { |         if (data.code === 0) { | ||||||
|         M.toast({html: "Program exited successfully."}); |           M.toast({html: "Program exited successfully."}); | ||||||
|         downloadButton.classList.remove('disabled'); |           downloadButton.classList.remove('disabled'); | ||||||
|       } else { |         } else { | ||||||
|         M.toast({html: `Program failed with code ${data.code}`}); |           M.toast({html: `Program failed with code ${data.code}`}); | ||||||
|  |         } | ||||||
|  |         stopLogsButton.innerHTML = "Close"; | ||||||
|  |         stopped = true; | ||||||
|       } |       } | ||||||
|       stopLogsButton.innerHTML = "Close"; |     }); | ||||||
|       stopped = true; |     logSocket.addEventListener('open', () => { | ||||||
|     } |       const msg = JSON.stringify({configuration: configuration}); | ||||||
|  |       logSocket.send(msg); | ||||||
|  |     }); | ||||||
|  |     logSocket.addEventListener('close', () => { | ||||||
|  |       if (!stopped) { | ||||||
|  |         M.toast({html: 'Terminated process.'}); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |     modalInstance.options.onCloseStart = () => { | ||||||
|  |       logSocket.close(); | ||||||
|  |     }; | ||||||
|   }); |   }); | ||||||
|   logSocket.addEventListener('open', () => { |  | ||||||
|     const msg = JSON.stringify({configuration: configuration}); |  | ||||||
|     logSocket.send(msg); |  | ||||||
|   }); |  | ||||||
|   logSocket.addEventListener('close', () => { |  | ||||||
|     if (!stopped) { |  | ||||||
|       M.toast({html: 'Terminated process.'}); |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
|   modalInstance.options.onCloseStart = () => { |  | ||||||
|     logSocket.close(); |  | ||||||
|   }; |  | ||||||
| }); |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
| const hassConfigModalElem = document.getElementById("modal-hass-config"); | const hassConfigModalElem = document.getElementById("modal-hass-config"); | ||||||
|  |  | ||||||
| document.querySelectorAll(".action-hass-config").forEach((btn) => { | document.querySelectorAll(".action-hass-config").forEach((btn) => { | ||||||
| btn.addEventListener('click', (e) => { |   btn.addEventListener('click', (e) => { | ||||||
|   configuration = e.target.getAttribute('data-node'); |     configuration = e.target.getAttribute('data-node'); | ||||||
|   const modalInstance = M.Modal.getInstance(hassConfigModalElem); |     const modalInstance = M.Modal.getInstance(hassConfigModalElem); | ||||||
|   const log = hassConfigModalElem.querySelector(".log"); |     const log = hassConfigModalElem.querySelector(".log"); | ||||||
|   log.innerHTML = ""; |     log.innerHTML = ""; | ||||||
|   const stopLogsButton = hassConfigModalElem.querySelector(".stop-logs"); |     const stopLogsButton = hassConfigModalElem.querySelector(".stop-logs"); | ||||||
|   let stopped = false; |     let stopped = false; | ||||||
|   stopLogsButton.innerHTML = "Stop"; |     stopLogsButton.innerHTML = "Stop"; | ||||||
|   modalInstance.open(); |     modalInstance.open(); | ||||||
|  |  | ||||||
|   const filenameField = hassConfigModalElem.querySelector('.filename'); |     const filenameField = hassConfigModalElem.querySelector('.filename'); | ||||||
|   filenameField.innerHTML = configuration; |     filenameField.innerHTML = configuration; | ||||||
|  |  | ||||||
|   const logSocket = new WebSocket(wsUrl + "/hass-config"); |     const logSocket = new WebSocket(wsUrl + "/hass-config"); | ||||||
|   logSocket.addEventListener('message', (event) => { |     logSocket.addEventListener('message', (event) => { | ||||||
|     const data = JSON.parse(event.data); |       const data = JSON.parse(event.data); | ||||||
|     if (data.event === "line") { |       if (data.event === "line") { | ||||||
|       const msg = data.data; |         const msg = data.data; | ||||||
|       log.innerHTML += colorReplace(msg); |         log.innerHTML += colorReplace(msg); | ||||||
|     } else if (data.event === "exit") { |       } else if (data.event === "exit") { | ||||||
|       if (data.code === 0) { |         if (data.code === 0) { | ||||||
|         M.toast({html: "Program exited successfully."}); |           M.toast({html: "Program exited successfully."}); | ||||||
|         downloadButton.classList.remove('disabled'); |           downloadButton.classList.remove('disabled'); | ||||||
|       } else { |         } else { | ||||||
|         M.toast({html: `Program failed with code ${data.code}`}); |           M.toast({html: `Program failed with code ${data.code}`}); | ||||||
|  |         } | ||||||
|  |         stopLogsButton.innerHTML = "Close"; | ||||||
|  |         stopped = true; | ||||||
|       } |       } | ||||||
|       stopLogsButton.innerHTML = "Close"; |     }); | ||||||
|       stopped = true; |     logSocket.addEventListener('open', () => { | ||||||
|     } |       const msg = JSON.stringify({configuration: configuration}); | ||||||
|  |       logSocket.send(msg); | ||||||
|  |     }); | ||||||
|  |     logSocket.addEventListener('close', () => { | ||||||
|  |       if (!stopped) { | ||||||
|  |         M.toast({html: 'Terminated process.'}); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |     modalInstance.options.onCloseStart = () => { | ||||||
|  |       logSocket.close(); | ||||||
|  |     }; | ||||||
|   }); |   }); | ||||||
|   logSocket.addEventListener('open', () => { |  | ||||||
|     const msg = JSON.stringify({configuration: configuration}); |  | ||||||
|     logSocket.send(msg); |  | ||||||
|   }); |  | ||||||
|   logSocket.addEventListener('close', () => { |  | ||||||
|     if (!stopped) { |  | ||||||
|       M.toast({html: 'Terminated process.'}); |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
|   modalInstance.options.onCloseStart = () => { |  | ||||||
|     logSocket.close(); |  | ||||||
|   }; |  | ||||||
| }); |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
| const editModalElem = document.getElementById("modal-editor"); | const editModalElem = document.getElementById("modal-editor"); | ||||||
| @@ -486,58 +487,59 @@ editor.session.setOption('tabSize', 2); | |||||||
|  |  | ||||||
| const saveButton = editModalElem.querySelector(".save-button"); | const saveButton = editModalElem.querySelector(".save-button"); | ||||||
| const saveEditor = () => { | const saveEditor = () => { | ||||||
| fetch(`/edit?configuration=${configuration}`, { |   fetch(`/edit?configuration=${configuration}`, { | ||||||
|     credentials: "same-origin", |       credentials: "same-origin", | ||||||
|     method: "POST", |       method: "POST", | ||||||
|     body: editor.getValue() |       body: editor.getValue() | ||||||
|   }).then(res => res.text()).then(() => { |     }).then(res => res.text()).then(() => { | ||||||
|     M.toast({ |       M.toast({ | ||||||
|       html: `Saved <code class="inlinecode">${configuration}</code>` |         html: `Saved <code class="inlinecode">${configuration}</code>` | ||||||
|  |       }); | ||||||
|     }); |     }); | ||||||
|   }); |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| editor.commands.addCommand({ | editor.commands.addCommand({ | ||||||
| name: 'saveCommand', |   name: 'saveCommand', | ||||||
| bindKey: {win: 'Ctrl-S',  mac: 'Command-S'}, |   bindKey: {win: 'Ctrl-S',  mac: 'Command-S'}, | ||||||
| exec: saveEditor, |   exec: saveEditor, | ||||||
| readOnly: false |   readOnly: false | ||||||
| }); | }); | ||||||
|  |  | ||||||
| saveButton.addEventListener('click', saveEditor); | saveButton.addEventListener('click', saveEditor); | ||||||
|  |  | ||||||
| document.querySelectorAll(".action-edit").forEach((btn) => { | document.querySelectorAll(".action-edit").forEach((btn) => { | ||||||
| btn.addEventListener('click', (e) => { |   btn.addEventListener('click', (e) => { | ||||||
|   configuration = e.target.getAttribute('data-node'); |     configuration = e.target.getAttribute('data-node'); | ||||||
|   const modalInstance = M.Modal.getInstance(editModalElem); |     const modalInstance = M.Modal.getInstance(editModalElem); | ||||||
|   const filenameField = editModalElem.querySelector('.filename'); |     const filenameField = editModalElem.querySelector('.filename'); | ||||||
|   filenameField.innerHTML = configuration; |     filenameField.innerHTML = configuration; | ||||||
|  |  | ||||||
|   fetch(`/edit?configuration=${configuration}`, {credentials: "same-origin"}) |     fetch(`/edit?configuration=${configuration}`, {credentials: "same-origin"}) | ||||||
|     .then(res => res.text()).then(response => { |       .then(res => res.text()).then(response => { | ||||||
|       editor.setValue(response, -1); |         editor.setValue(response, -1); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     modalInstance.open(); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   modalInstance.open(); |  | ||||||
| }); |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
| const modalSetupElem = document.getElementById("modal-wizard"); | const modalSetupElem = document.getElementById("modal-wizard"); | ||||||
| const setupWizardStart = document.getElementById('setup-wizard-start'); | const setupWizardStart = document.getElementById('setup-wizard-start'); | ||||||
| const startWizard = () => { | const startWizard = () => { | ||||||
| const modalInstance = M.Modal.getInstance(modalSetupElem); |   const modalInstance = M.Modal.getInstance(modalSetupElem); | ||||||
| modalInstance.open(); |   modalInstance.open(); | ||||||
|  |  | ||||||
| modalInstance.options.onCloseStart = () => { |   modalInstance.options.onCloseStart = () => { | ||||||
|  |  | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   $('.stepper').activateStepper({ | ||||||
|  |     linearStepsNavigation: false, | ||||||
|  |     autoFocusInput: true, | ||||||
|  |     autoFormCreation: true, | ||||||
|  |     showFeedbackLoader: true, | ||||||
|  |     parallel: false | ||||||
|  |   }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| $('.stepper').activateStepper({ |  | ||||||
|   linearStepsNavigation: false, |  | ||||||
|   autoFocusInput: true, |  | ||||||
|   autoFormCreation: true, |  | ||||||
|   showFeedbackLoader: true, |  | ||||||
|   parallel: false |  | ||||||
| }); |  | ||||||
| }; |  | ||||||
| setupWizardStart.addEventListener('click', startWizard); | setupWizardStart.addEventListener('click', startWizard); | ||||||
| @@ -49,12 +49,18 @@ def cpp_string_escape(string, encoding='utf-8'): | |||||||
|     return '"' + result + '"' |     return '"' + result + '"' | ||||||
|  |  | ||||||
|  |  | ||||||
| def color(the_color, message='', reset=None): | def color(the_color, message=''): | ||||||
|     """Color helper.""" |     from esphomeyaml import core | ||||||
|     from colorlog.escape_codes import escape_codes, parse_colors |     from colorlog.escape_codes import escape_codes, parse_colors | ||||||
|  |  | ||||||
|     if not message: |     if not message: | ||||||
|         return parse_colors(the_color) |         res = parse_colors(the_color) | ||||||
|     return parse_colors(the_color) + message + escape_codes[reset or 'reset'] |     else: | ||||||
|  |         res = parse_colors(the_color) + message + escape_codes['reset'] | ||||||
|  |  | ||||||
|  |     if core.FROM_DASHBOARD: | ||||||
|  |         res = res.replace('\033', '\\033') | ||||||
|  |     return res | ||||||
|  |  | ||||||
|  |  | ||||||
| def run_system_command(*args): | def run_system_command(*args): | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ import sys | |||||||
|  |  | ||||||
| import paho.mqtt.client as mqtt | import paho.mqtt.client as mqtt | ||||||
|  |  | ||||||
|  | from esphomeyaml import core | ||||||
| from esphomeyaml.const import CONF_BROKER, CONF_DISCOVERY_PREFIX, CONF_ESPHOMEYAML, \ | from esphomeyaml.const import CONF_BROKER, CONF_DISCOVERY_PREFIX, CONF_ESPHOMEYAML, \ | ||||||
|     CONF_LOG_TOPIC, CONF_MQTT, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SSL_FINGERPRINTS, \ |     CONF_LOG_TOPIC, CONF_MQTT, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SSL_FINGERPRINTS, \ | ||||||
|     CONF_TOPIC, CONF_TOPIC_PREFIX, CONF_USERNAME |     CONF_TOPIC, CONF_TOPIC_PREFIX, CONF_USERNAME | ||||||
| @@ -54,7 +55,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, escape=False): | def show_logs(config, topic=None, username=None, password=None, client_id=None): | ||||||
|     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: | ||||||
| @@ -73,7 +74,7 @@ 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]') | ||||||
|         message = time + msg.payload |         message = time + msg.payload | ||||||
|         if escape: |         if core.FROM_DASHBOARD: | ||||||
|             message = message.replace('\033', '\\033') |             message = message.replace('\033', '\\033') | ||||||
|         safe_print(message) |         safe_print(message) | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user