diff --git a/.editorconfig b/.editorconfig index d02b3d9..74fb905 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,7 +9,9 @@ charset = utf-8 insert_final_newline = true trim_trailing_whitespace = true indent_style = space -max_line_length = 90 +max_line_length = 180 +indent_size = 2 + [**.py] indent_size = 4 diff --git a/octoprint_klipper/__init__.py b/octoprint_klipper/__init__.py index e258d0c..1b511d8 100644 --- a/octoprint_klipper/__init__.py +++ b/octoprint_klipper/__init__.py @@ -14,16 +14,22 @@ # along with this program. If not, see . from __future__ import absolute_import, division, print_function, unicode_literals -import datetime import logging import octoprint.plugin import octoprint.plugin.core import glob import os +import time import sys + +from octoprint.server import NO_CONTENT +from octoprint.util import is_hidden_path +from octoprint.util import get_formatted_size +from . import util, cfgUtils, logger from octoprint.util.comm import parse_firmware_line from octoprint.access.permissions import Permissions, ADMIN_GROUP from .modules import KlipperLogAnalyzer +from octoprint.server.util.flask import restricted_access import flask from flask_babel import gettext @@ -35,13 +41,16 @@ except ImportError: if sys.version_info[0] < 3: import StringIO +MAX_UPLOAD_SIZE = 5 * 1024 * 1024 # 5Mb + class KlipperPlugin( octoprint.plugin.StartupPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.SettingsPlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.SimpleApiPlugin, - octoprint.plugin.EventHandlerPlugin): + octoprint.plugin.EventHandlerPlugin, + octoprint.plugin.BlueprintPlugin): _parsing_response = False _parsing_check_response = True @@ -74,8 +83,10 @@ class KlipperPlugin( self._settings.global_set( ["serial", "additionalPorts"], additional_ports) self._settings.save() - self.log_info( - "Added klipper serial port {} to list of additional ports.".format(klipper_port)) + logger.log_info( + self, + "Added klipper serial port {} to list of additional ports.".format(klipper_port) + ) # -- Settings Plugin @@ -126,6 +137,7 @@ class KlipperPlugin( debug_logging=False, configpath="~/printer.cfg", old_config="", + temp_config="", logpath="/tmp/klippy.log", reload_command="RESTART", shortStatus_navbar=True, @@ -141,71 +153,32 @@ class KlipperPlugin( configpath = os.path.expanduser( self._settings.get(["configuration", "configpath"]) ) - data["config"] = "" - try: - f = open(configpath, "r") - data["config"] = f.read() - f.close() - except IOError: - self.log_error( - "Error: Klipper config file not found at: {}".format( - configpath) - ) - else: - self.send_message("reload", "config", "", data["config"]) + standardconfigfile = os.path.join(configpath, "printer.cfg") + data["config"] = cfgUtils.get_cfg(self, standardconfigfile) + util.send_message(self, "reload", "config", "", data["config"]) # send the configdata to frontend to update ace editor return data def on_settings_save(self, data): - - self.log_debug( - "Save klipper configs" - ) - if "config" in data: - check_parse = self._settings.get(["configuration", "parse_check"]) - self.log_debug("check_parse: {}".format(check_parse)) - - if sys.version_info[0] < 3: - data["config"] = data["config"].encode('utf-8') - - # check for configpath if it was changed during changing of the configfile - if self.key_exist(data, "configuration", "configpath"): - configpath = os.path.expanduser( - data["configuration"]["configpath"] - ) - else: - # if the configpath was not changed during changing the printer.cfg. Then the configpath would not be in data[] - configpath = os.path.expanduser( - self._settings.get(["configuration", "configpath"]) - ) - if self.file_exist(configpath): - self.copy_cfg_to_backup(configpath) - - if self._parsing_check_response or not check_parse: - try: - f = open(configpath, "w") - f.write(data["config"]) - f.close() - - self.log_debug("Writing Klipper config to {}".format(configpath)) - except IOError: - self.log_error("Error: Couldn't write Klipper config file: {}".format(configpath)) + if cfgUtils.save_cfg(self, data["config"], "printer.cfg"): + #load the reload command from changed data if it is not existing load the saved setting + if util.key_exist(data, "configuration", "reload_command"): + reload_command = os.path.expanduser( + data["configuration"]["reload_command"] + ) else: - #load the reload command from changed data if it is not existing load the saved setting - if self.key_exist(data, "configuration", "reload_command"): - reload_command = os.path.expanduser( - data["configuration"]["reload_command"] - ) - else: - reload_command = self._settings.get(["configuration", "reload_command"]) + reload_command = self._settings.get(["configuration", "reload_command"]) - if reload_command != "manually": - # Restart klippy to reload config - self._printer.commands(reload_command) - self.log_info("Restarting Klipper.") - # we dont want to write the klipper conf to the octoprint settings - data.pop("config", None) + if reload_command != "manually": + # Restart klippy to reload config + self._printer.commands(reload_command) + logger.log_info(self, "Restarting Klipper.") + # we dont want to write the klipper conf to the octoprint settings + else: + # save not sure. saving to the octoprintconfig: + self._settings.set(["configuration", "temp_config"], data) + data.pop("config", None) # save the rest of changed settings into config.yaml of octoprint old_debug_logging = self._settings.get_boolean(["configuration", "debug_logging"]) @@ -280,14 +253,14 @@ class KlipperPlugin( settings.remove(["probePoints"]) if settings.has(["configPath"]): - self.log_info("migrate setting for: configPath") + logger.log_info(self, "migrate setting for: configPath") settings.set(["config_path"], settings.get(["configPath"])) settings.remove(["configPath"]) if current is not None and current < 3: settings = self._settings if settings.has(["configuration", "navbar"]): - self.log_info("migrate setting for: configuration/navbar") + logger.log_info(self, "migrate setting for: configuration/navbar") settings.set(["configuration", "shortStatus_navbar"], settings.get(["configuration", "navbar"])) settings.remove(["configuration", "navbar"]) @@ -323,17 +296,29 @@ class KlipperPlugin( custom_bindings=True ), dict(type="sidebar", - custom_bindings=True, - icon="rocket", - replaces="connection" if self._settings.get_boolean( - ["connection", "replace_connection_panel"]) else "" - ), + custom_bindings=True, + icon="rocket", + replaces="connection" if self._settings.get_boolean( + ["connection", "replace_connection_panel"]) else "" + ), dict( type="generic", name="Performance Graph", template="klipper_graph_dialog.jinja2", custom_bindings=True ), + dict( + type="generic", + name="Config Backups", + template="klipper_backups_dialog.jinja2", + custom_bindings=True + ), + dict( + type="generic", + name="Config Editor", + template="klipper_editor.jinja2", + custom_bindings=True + ), dict( type="generic", name="Macro Dialog", @@ -342,6 +327,12 @@ class KlipperPlugin( ) ] + def get_template_vars(self): + return { + "max_upload_size": MAX_UPLOAD_SIZE, + "max_upload_size_str": get_formatted_size(MAX_UPLOAD_SIZE), + } + # -- Asset Plugin def get_assets(self): @@ -352,8 +343,11 @@ class KlipperPlugin( "js/klipper_pid_tuning.js", "js/klipper_offset.js", "js/klipper_param_macro.js", - "js/klipper_graph.js" - ], + "js/klipper_graph.js", + "js/klipper_backup.js", + "js/klipper_editor.js" + ], + clientjs=["clientjs/klipper.js"], css=["css/klipper.css"] ) @@ -361,18 +355,19 @@ class KlipperPlugin( def on_event(self, event, payload): if "UserLoggedIn" == event: - self.update_status("info", "Klipper: Standby") + util.update_status(self, "info", "Klipper: Standby") if "Connecting" == event: - self.update_status("info", "Klipper: Connecting ...") + util.update_status(self, "info", "Klipper: Connecting ...") elif "Connected" == event: - self.update_status("info", "Klipper: Connected to host") - self.log_info( + util.update_status(self, "info", "Klipper: Connected to host") + logger.log_info( + self, "Connected to host via {} @{}bps".format(payload["port"], payload["baudrate"])) elif "Disconnected" == event: - self.update_status("info", "Klipper: Disconnected from host") + util.update_status(self, "info", "Klipper: Disconnected from host") elif "Error" == event: - self.update_status("error", "Klipper: Error") - self.log_error(payload["error"]) + util.update_status(self, "error", "Klipper: Error") + logger.log_error(self, payload["error"]) # -- GCODE Hook @@ -381,22 +376,22 @@ class KlipperPlugin( if "FIRMWARE_VERSION" in line: printerInfo = parse_firmware_line(line) if "FIRMWARE_VERSION" in printerInfo: - self.log_info("Firmware version: {}".format( + logger.log_info(self, "Firmware version: {}".format( printerInfo["FIRMWARE_VERSION"])) elif "// probe" in line or "// Failed to verify BLTouch" in line: msg = line.strip('/') - self.log_info(msg) + logger.log_info(self, msg) self.write_parsing_response_buffer() elif "//" in line: # add lines with // to a buffer self._message = self._message + line.strip('/') if not self._parsing_response: - self.update_status("info", self._message) + util.update_status(self, "info", self._message) self._parsing_response = True elif "!!" in line: msg = line.strip('!') - self.update_status("error", msg) - self.log_error(msg) + util.update_status(self, "error", msg) + logger.log_error(self, msg) self.write_parsing_response_buffer() else: self.write_parsing_response_buffer() @@ -406,7 +401,7 @@ class KlipperPlugin( # write buffer with // lines after a gcode response without // if self._parsing_response: self._parsing_response = False - self.log_info(self._message) + logger.log_info(self, self._message) self._message = "" def get_api_commands(self): @@ -414,7 +409,6 @@ class KlipperPlugin( listLogFiles=[], getStats=["logFile"], reloadConfig=[], - reloadCfgBackup=[], checkConfig=["config"] ) @@ -424,12 +418,12 @@ class KlipperPlugin( logpath = os.path.expanduser( self._settings.get(["configuration", "logpath"]) ) - if self.file_exist(logpath): + if util.file_exist(self, logpath): for f in glob.glob(self._settings.get(["configuration", "logpath"]) + "*"): filesize = os.path.getsize(f) + filemdate = time.strftime("%d.%m.%Y %H:%M",time.localtime(os.path.getctime(f))) files.append(dict( - name=os.path.basename( - f) + " ({:.1f} KB)".format(filesize / 1000.0), + name=os.path.basename(f) + " (" + filemdate + ")", file=f, size=filesize )) @@ -441,30 +435,151 @@ class KlipperPlugin( log_analyzer = KlipperLogAnalyzer.KlipperLogAnalyzer( data["logFile"]) return flask.jsonify(log_analyzer.analyze()) - elif command == "reloadConfig": - self.log_debug("reloadConfig") - return self.reload_cfg() - elif command == "reloadCfgBackup": - self.log_debug("reloadCfgBackup") - configpath = os.path.expanduser( - self._settings.get(["configuration", "configpath"]) - ) - return self.copy_cfg_from_backup(configpath) - elif command == "checkConfig": - if "config" in data: - #self.write_cfg_backup(data["config"]) - if self.key_exist(data, "configuration", "parse_check"): - check_parse = data["configuration"]["parse_check"] - else: - check_parse = self._settings.get(["configuration", "parse_check"]) - if check_parse and not self.validate_configfile(data["config"]): - self.log_debug("validateConfig not ok") - self._settings.set(["configuration", "old_config"], data["config"]) - return flask.jsonify(checkConfig="not OK") - else: - self.log_debug("validateConfig ok") - self._settings.set(["configuration", "old_config"], "") - return flask.jsonify(checkConfig="OK") + + def is_blueprint_protected(self): + return False + + def route_hook(self, server_routes, *args, **kwargs): + from octoprint.server.util.tornado import LargeResponseHandler, path_validation_factory + from octoprint.util import is_hidden_path + configpath = os.path.expanduser( + self._settings.get(["configuration", "configpath"]) + ) + bak_path = os.path.join(self.get_plugin_data_folder(), "configs", "") + + return [ + (r"/download/(.*)", LargeResponseHandler, dict(path=configpath, + as_attachment=True, + path_validation=path_validation_factory(lambda path: not is_hidden_path(path), + status_code=404))), + (r"/download/backup(.*)", LargeResponseHandler, dict(path=bak_path, + as_attachment=True, + path_validation=path_validation_factory(lambda path: not is_hidden_path(path), + status_code=404))) + ] + + # API for Backups + # Get Content of a Backupconfig + @octoprint.plugin.BlueprintPlugin.route("/backup/", methods=["GET"]) + @restricted_access + @Permissions.PLUGIN_KLIPPER_CONFIG.require(403) + def get_backup(self, filename): + data_folder = self.get_plugin_data_folder() + full_path = os.path.realpath(os.path.join(data_folder, "configs", filename)) + response = cfgUtils.get_cfg(self, full_path) + return flask.jsonify(response = response) + + # Delete a Backupconfig + @octoprint.plugin.BlueprintPlugin.route("/backup/", methods=["DELETE"]) + @restricted_access + @Permissions.PLUGIN_KLIPPER_CONFIG.require(403) + def delete_backup(self, filename): + data_folder = self.get_plugin_data_folder() + full_path = os.path.realpath(os.path.join(data_folder, "configs", filename)) + if ( + full_path.startswith(data_folder) + and os.path.exists(full_path) + and not is_hidden_path(full_path) + ): + try: + os.remove(full_path) + except Exception: + self._octoklipper_logger.exception("Could not delete {}".format(filename)) + raise + return NO_CONTENT + + # Get a list of all backuped configfiles + @octoprint.plugin.BlueprintPlugin.route("/backup/list", methods=["GET"]) + @restricted_access + @Permissions.PLUGIN_KLIPPER_CONFIG.require(403) + def list_backups(self): + files = cfgUtils.list_cfg_files(self, "backup") + return flask.jsonify(files = files) + + # restore a backuped configfile + @octoprint.plugin.BlueprintPlugin.route("/backup/restore/", methods=["GET"]) + @restricted_access + @Permissions.PLUGIN_KLIPPER_CONFIG.require(403) + def restore_backup(self, filename): + configpath = os.path.expanduser( + self._settings.get(["configuration", "configpath"]) + ) + data_folder = self.get_plugin_data_folder() + backupfile = os.path.realpath(os.path.join(data_folder, "configs", filename)) + return flask.jsonify(restored = cfgUtils.copy_cfg(self, backupfile, configpath)) + + # API for Configs + # Get Content of a Configfile + @octoprint.plugin.BlueprintPlugin.route("/config/", methods=["GET"]) + @restricted_access + @Permissions.PLUGIN_KLIPPER_CONFIG.require(403) + def get_config(self, filename): + cfg_path = os.path.expanduser( + self._settings.get(["configuration", "configpath"]) + ) + full_path = os.path.realpath(os.path.join(cfg_path, filename)) + response = cfgUtils.get_cfg(self, full_path) + return flask.jsonify(response = response) + + # Delete a Configfile + @octoprint.plugin.BlueprintPlugin.route("/config/", methods=["DELETE"]) + @restricted_access + @Permissions.PLUGIN_KLIPPER_CONFIG.require(403) + def delete_config(self, filename): + cfg_path = os.path.expanduser( + self._settings.get(["configuration", "configpath"]) + ) + full_path = os.path.realpath(os.path.join(cfg_path, filename)) + if ( + full_path.startswith(cfg_path) + and os.path.exists(full_path) + and not is_hidden_path(full_path) + ): + try: + os.remove(full_path) + except Exception: + self._octoklipper_logger.exception("Could not delete {}".format(filename)) + raise + return NO_CONTENT + + # Get a list of all configfiles + @octoprint.plugin.BlueprintPlugin.route("/config/list", methods=["GET"]) + @restricted_access + @Permissions.PLUGIN_KLIPPER_CONFIG.require(403) + def list_configs(self): + files = cfgUtils.list_cfg_files(self, "") + return flask.jsonify(files = files, max_upload_size = MAX_UPLOAD_SIZE) + + # check syntax of a given data + @octoprint.plugin.BlueprintPlugin.route("/config/check", methods=["POST"]) + @restricted_access + @Permissions.PLUGIN_KLIPPER_CONFIG.require(403) + def check_config(self): + data = flask.request.json + data_to_check = data.get("DataToCheck", []) + response = cfgUtils.check_cfg(self, data_to_check) + return flask.jsonify(is_syntax_ok = response) + + # save a configfile + @octoprint.plugin.BlueprintPlugin.route("/config/save", methods=["POST"]) + @restricted_access + @Permissions.PLUGIN_KLIPPER_CONFIG.require(403) + def save_config(self): + data = flask.request.json + + filename = data.get("filename", []) + Filecontent = data.get("DataToSave", []) + if filename == []: + flask.abort( + 400, + description="Invalid request, the filename is not set", + ) + saved = cfgUtils.save_cfg(self, Filecontent, filename) + if saved == True: + util.send_message(self, "reload", "configlist", "", "") + return flask.jsonify(saved = saved) + + # APIs end def get_update_information(self): return dict( @@ -490,193 +605,6 @@ class KlipperPlugin( ) ) - #-- Helpers - def send_message(self, type, subtype, title, payload): - """ - Send Message over API to FrontEnd - """ - self._plugin_manager.send_plugin_message( - self._identifier, - dict( - time=datetime.datetime.now().strftime("%H:%M:%S"), - type=type, - subtype=subtype, - title=title, - payload=payload - ) - ) - - def poll_status(self): - self._printer.commands("STATUS") - - def update_status(self, type, status): - self.send_message("status", type, status, status) - - def log_info(self, message): - self._octoklipper_logger.info(message) - self.send_message("log", "info", message, message) - - def log_debug(self, message): - self._octoklipper_logger.debug(message) - self._logger.info(message) - # sends a message to frontend(in klipper.js -> self.onDataUpdaterPluginMessage) and write it to the console. - # _mtype, subtype=debug/info, title of message, message) - self.send_message("console", "debug", message, message) - - def log_error(self, error): - self._octoklipper_logger.error(error) - self._logger.info(error) - self.send_message("log", "error", error, error) - - def file_exist(self, filepath): - if not os.path.isfile(filepath): - self.send_message("PopUp", "warning", "OctoKlipper Settings", - "Klipper " + filepath + " does not exist!") - return False - else: - return True - - def key_exist(self, dict, key1, key2): - try: - dict[key1][key2] - except KeyError: - return False - else: - return True - - def validate_configfile(self, dataToBeValidated): - """ - --->SyntaxCheck for a given data<---- - """ - check_parse = self._settings.get(["configuration", "parse_check"]) - if check_parse: - try: - dataToValidated = configparser.RawConfigParser(strict=False) - # - if sys.version_info[0] < 3: - buf = StringIO.StringIO(dataToBeValidated) - dataToValidated.readfp(buf) - else: - dataToValidated.read_string(dataToBeValidated) - - sections_search_list = ["bltouch", - "probe"] - value_search_list = [ "x_offset", - "y_offset", - "z_offset"] - try: - # cycle through sections and then values - for y in sections_search_list: - for x in value_search_list: - if dataToValidated.has_option(y, x): - a_float = dataToValidated.getfloat(y, x) - except ValueError as error: - self.log_error( - "Error: Invalid Value for "+x+" in Section: "+y+"\n" + - "{}".format(str(error)) - ) - self.send_message("PopUp", "warning", "OctoKlipper: Invalid Config\n", - "Config got not saved!\n" + - "You can reload your last changes\n" + - "on the 'Klipper Configuration' tab.\n\n" + - "Invalid Value for "+x+" in Section: "+y+"\n" + "{}".format(str(error))) - self._parsing_check_response = False - return False - except configparser.Error as error: - if sys.version_info[0] < 3: - error.message = error.message.replace("\\n","") - error.message = error.message.replace("file: u","Klipper Configuration", 1) - error.message = error.message.replace("'","", 2) - error.message = error.message.replace("u'","'", 1) - - else: - error.message = error.message.replace("\\n","") - error.message = error.message.replace("file:","Klipper Configuration", 1) - error.message = error.message.replace("'","", 2) - self.log_error( - "Error: Invalid Klipper config file:\n" + - "{}".format(str(error)) - ) - self.send_message("PopUp", "warning", "OctoKlipper: Invalid Config data\n", - "Config got not saved!\n" + - "You can reload your last changes\n" + - "on the 'Klipper Configuration' tab.\n\n" + str(error)) - self._parsing_check_response = False - return False - else: - self._parsing_check_response = True - return True - - def reload_cfg(self): - data = octoprint.plugin.SettingsPlugin.on_settings_load(self) - - configpath = os.path.expanduser( - self._settings.get(["configuration", "configpath"]) - ) - - try: - f = open(configpath, "r") - data["config"] = f.read() - f.close() - except IOError: - self.log_error( - "Error: Klipper config file not found at: {}".format( - configpath) - ) - else: - - self._settings.set(["config"], data["config"]) - # self.send_message("reload", "config", "", data["config"]) - # send the configdata to frontend to update ace editor - if sys.version_info[0] < 3: - data["config"] = data["config"].decode('utf-8') - return flask.jsonify(data=data["config"]) - - def copy_cfg_from_backup(self, dst): - """ - Copy the backuped config files in the data folder of OctoKlipper to the given destination - """ - from shutil import copy - - bak_config_path = os.path.join(self.get_plugin_data_folder(), "configs/") - src_files = os.listdir(bak_config_path) - nio_files = [] - self.log_debug("reloadCfgBackupPath:" + src_files) - - for file_name in src_files: - full_file_name = os.path.join(bak_config_path, file_name) - if os.path.isfile(full_file_name): - try: - copy(full_file_name, dst) - except IOError: - self.log_error( - "Error: Klipper config file not found at: {}".format( - full_file_name) - ) - nio_files.append(full_file_name) - else: - self.log_debug("File done: " + full_file_name) - return nio_files - - def copy_cfg_to_backup(self, src): - """ - Copy the config file into the data folder of OctoKlipper - """ - from shutil import copyfile - - filename = os.path.basename(src) - dst = os.path.join(self.get_plugin_data_folder(), "configs", "", filename) - self.log_debug("CopyCfgBackupPath:" + dst) - try: - copyfile(src, dst) - except IOError: - self.log_error( - "Error: Couldn't copy Klipper config file to {}".format( - dst) - ) - else: - self.log_debug("CfgBackup writen") - __plugin_name__ = "OctoKlipper" __plugin_pythoncompat__ = ">=2.7,<4" __plugin_settings_overlay__ = { @@ -690,12 +618,12 @@ __plugin_settings_overlay__ = { } } - def __plugin_load__(): global __plugin_implementation__ global __plugin_hooks__ __plugin_implementation__ = KlipperPlugin() __plugin_hooks__ = { + "octoprint.server.http.routes": __plugin_implementation__.route_hook, "octoprint.access.permissions": __plugin_implementation__.get_additional_permissions, "octoprint.comm.protocol.gcode.received": __plugin_implementation__.on_parse_gcode, "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information diff --git a/octoprint_klipper/cfgUtils.py b/octoprint_klipper/cfgUtils.py new file mode 100644 index 0000000..e4af2cb --- /dev/null +++ b/octoprint_klipper/cfgUtils.py @@ -0,0 +1,285 @@ +from __future__ import absolute_import, division, print_function, unicode_literals +import glob +import os, time, sys + +from . import util, logger +from octoprint.util import is_hidden_path +import flask +from flask_babel import gettext + +def list_cfg_files(self, path: str) -> list: + """Generate list of config files. + + Args: + path (str): Path to the config files. + + Returns: + list: for every file a dict with keys for name, file, size, mdate, url. + """ + + files = [] + if path=="backup": + cfg_path = os.path.join(self.get_plugin_data_folder(), "configs", "*") + else: + cfg_path = os.path.expanduser( + self._settings.get(["configuration", "configpath"]) + ) + cfg_path = os.path.join(cfg_path, "*.cfg") + cfg_files = glob.glob(cfg_path) + logger.log_debug(self, "list_cfg_files Path: " + cfg_path) + + f_counter = 1 + for f in cfg_files: + filesize = os.path.getsize(f) + filemdate = time.localtime(os.path.getmtime(f)) + files.append(dict( + name=os.path.basename(f), + file=f, + size=" ({:.1f} KB)".format(filesize / 1000.0), + mdate=time.strftime("%d.%m.%Y %H:%M", filemdate), + url= flask.url_for("index") + + "plugin/klipper/download/" + + os.path.basename(f), + )) + logger.log_debug(self, "list_cfg_files " + str(f_counter) + ": " + f) + f_counter += 1 + return files + +def get_cfg(self, file): + """Get the content of a configuration file. + + Args: + file (str): The name of the file to read + + Returns: + dict: + config (str): The configuration of the file + text (str): The text of the error + """ + + response = {"config":"", + "text": ""} + if not file: + cfg_path = os.path.expanduser( + self._settings.get(["configuration", "configpath"]) + ) + file = os.path.join(cfg_path, "printer.cfg") + if util.file_exist(self, file): + logger.log_debug(self, "get_cfg_files Path: " + file) + try: + with open(file, "r") as f: + response['config'] = f.read() + except IOError as Err: + logger.log_error( + self, + "Error: Klipper config file not found at: {}".format(file) + + "\n IOError: {}".format(Err) + ) + response['text'] = Err + return response + else: + if sys.version_info[0] < 3: + response['config'] = response.config.decode('utf-8') + return response + finally: + f.close() + else: + response['text'] = gettext("File not found!") + return response + +def save_cfg(self, content, filename="printer.cfg"): + """Save the configuration file to given file. + + Args: + content (str): The content of the configuration. + filename (str): The filename of the configuration file. Default is "printer.cfg" + + Returns: + bool: True if the configuration file was saved successfully. Otherwise False + """ + + logger.log_debug( + self, + "Save klipper config" + ) + + if sys.version_info[0] < 3: + content = content.encode('utf-8') + check_parse = self._settings.get(["configuration", "parse_check"]) + logger.log_debug(self, "check_parse on filesave: {}".format(check_parse)) + configpath = os.path.expanduser(self._settings.get(["configuration", "configpath"])) + filepath = os.path.join(configpath, filename) + + logger.log_debug(self, "save filepath: {}".format(filepath)) + + self._settings.set(["configuration", "temp_config"], content) + check = True + if check_parse: + check=check_cfg(self, content) + if check == True: + try: + logger.log_debug(self, "Writing Klipper config to {}".format(filepath)) + with open(filepath, "w") as f: + f.write(content) + except IOError: + logger.log_error(self, "Error: Couldn't open Klipper config file: {}".format(filepath)) + return False + else: + logger.log_debug(self, "Writen Klipper config to {}".format(filepath)) + return True + finally: + f.close() + copy_cfg_to_backup(self, filepath) + else: + return False + +def check_cfg(self, data): + """Checks the given data on parsing errors. + + Args: + data (str): Content to be validated. + + Returns: + bool: True if the data is valid. False if it is not. + """ + + try: + import configparser + except ImportError: + import ConfigParser as configparser + + try: + dataToValidated = configparser.RawConfigParser(strict=False) + if sys.version_info[0] < 3: + import StringIO + buf = StringIO.StringIO(data) + dataToValidated.readfp(buf) + else: + dataToValidated.read_string(data) + + sections_search_list = ["bltouch", + "probe"] + value_search_list = [ "x_offset", + "y_offset", + "z_offset"] + try: + # cycle through sections and then values + for y in sections_search_list: + for x in value_search_list: + if dataToValidated.has_option(y, x): + a_float = dataToValidated.getfloat(y, x) + if a_float: + pass + except ValueError as error: + logger.log_error( + self, + "Error: Invalid Value for "+x+" in Section: "+y+"\n" + + "{}".format(str(error)) + ) + util.send_message( + self, + "PopUp", + "warning", + "OctoKlipper: Invalid Config\n", + "Config got not saved!\n\n" + + "Invalid Value for "+x+" in Section: "+y+"\n" + + "{}".format(str(error)) + ) + return False + except configparser.Error as error: + if sys.version_info[0] < 3: + error.message = error.message.replace("\\n","") + error.message = error.message.replace("file: u","Klipper Configuration", 1) + error.message = error.message.replace("'","", 2) + error.message = error.message.replace("u'","'", 1) + + else: + error.message = error.message.replace("\\n","") + error.message = error.message.replace("file:","Klipper Configuration", 1) + error.message = error.message.replace("'","", 2) + logger.log_error( + self, + "Error: Invalid Klipper config file:\n" + + "{}".format(str(error)) + ) + util.send_message(self, "PopUp", "warning", "OctoKlipper: Invalid Config data\n", + "Config got not saved!\n\n" + + str(error)) + + return False + else: + return True + +def copy_cfg(self, file, dst): + """Copy the config file to the destination. + + Args: + file (str): Filepath of the config file to copy. + dst (str): Path to copy the config file to. + + Returns: + bool: True if the copy succeeded, False otherwise. + """ + from shutil import copy + + if os.path.isfile(file): + try: + copy(file, dst) + except IOError: + logger.log_error( + self, + "Error: Klipper config file not found at: {}".format(file) + ) + return False + else: + logger.log_debug( + self, + "File copied: " + + file + ) + return True + return False + +def copy_cfg_to_backup(self, src): + """Copy the config file to backup directory of OctoKlipper. + + Args: + src (str): Path to the config file to copy. + + Returns: + bool: True if the config file was copied successfully. False otherwise. + """ + from shutil import copyfile + + if os.path.isfile(src): + cfg_path = os.path.join(self.get_plugin_data_folder(), "configs", "") + filename = os.path.basename(src) + if not os.path.exists(cfg_path): + try: + os.mkdir(cfg_path) + except OSError: + logger.log_error(self, "Error: Creation of the backup directory {} failed".format(cfg_path)) + return False + else: + logger.log_debug(self, "Directory {} created".format(cfg_path)) + + dst = os.path.join(cfg_path, filename) + logger.log_debug(self, "copy_cfg_to_backup:" + src + " to " + dst) + if not src == dst: + try: + copyfile(src, dst) + except IOError: + logger.log_error( + self, + "Error: Couldn't copy Klipper config file to {}".format(dst) + ) + return False + else: + logger.log_debug(self, "CfgBackup " + dst + " writen") + return True + else: + return False + else: + return False + + diff --git a/octoprint_klipper/logger.py b/octoprint_klipper/logger.py new file mode 100644 index 0000000..46ae70f --- /dev/null +++ b/octoprint_klipper/logger.py @@ -0,0 +1,17 @@ +from . import util + +def log_info(self, message): + self._octoklipper_logger.info(message) + util.send_message(self, "log", "info", message, message) + +def log_debug(self, message): + self._octoklipper_logger.debug(message) + self._logger.info(message) + # sends a message to frontend(in klipper.js -> self.onDataUpdaterPluginMessage) and write it to the console. + # _mtype, subtype=debug/info, title of message, message) + util.send_message(self, "console", "debug", message, message) + +def log_error(self, error): + self._octoklipper_logger.error(error) + self._logger.error(error) + util.send_message(self, "log", "error", error, error) diff --git a/octoprint_klipper/modules/KlipperLogAnalyzer.py b/octoprint_klipper/modules/KlipperLogAnalyzer.py index 3f1881d..5bce9ff 100644 --- a/octoprint_klipper/modules/KlipperLogAnalyzer.py +++ b/octoprint_klipper/modules/KlipperLogAnalyzer.py @@ -15,6 +15,7 @@ import flask import optparse, datetime +from .. import logger class KlipperLogAnalyzer(): MAXBANDWIDTH=25000. @@ -81,6 +82,7 @@ class KlipperLogAnalyzer(): out.append(keyparts) f.close() except IOError: + logger.log_error(self, "Couldn't open log file: {}".format(logname)) print("Couldn't open log file") return out diff --git a/octoprint_klipper/static/clientjs/klipper.js b/octoprint_klipper/static/clientjs/klipper.js new file mode 100644 index 0000000..aedc85d --- /dev/null +++ b/octoprint_klipper/static/clientjs/klipper.js @@ -0,0 +1,72 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define(["OctoPrintClient"], factory); + } else { + factory(global.OctoPrintClient); + } +})(this, function (OctoPrintClient) { + var OctoKlipperClient = function (base) { + this.base = base; + this.url = this.base.getBlueprintUrl("klipper"); + }; + + OctoKlipperClient.prototype.getCfg = function (config, opts) { + return this.base.get(this.url + "config/" + config, opts); + }; + + OctoKlipperClient.prototype.getCfgBak = function (backup, opts) { + return this.base.get(this.url + "backup/" + backup, opts); + }; + + OctoKlipperClient.prototype.listCfg = function (opts) { + return this.base.get(this.url + "config/list", opts); + }; + + OctoKlipperClient.prototype.listCfgBak = function (opts) { + return this.base.get(this.url + "backup/list", opts); + }; + + OctoKlipperClient.prototype.checkCfg = function (content, opts) { + content = content || []; + + var data = { + DataToCheck: content, + }; + + return this.base.postJson(this.url + "config/check", data, opts); + }; + + OctoKlipperClient.prototype.saveCfg = function (content, filename, opts) { + content = content || []; + filename = filename || []; + + var data = { + DataToSave: content, + filename: filename, + }; + + return this.base.postJson(this.url + "config/save", data, opts); + }; + + OctoKlipperClient.prototype.deleteCfg = function (config, opts) { + return this.base.delete(this.url + "config/" + config, opts); + }; + + OctoKlipperClient.prototype.deleteBackup = function (backup, opts) { + return this.base.delete(this.url + "backup/" + backup, opts); + }; + + OctoKlipperClient.prototype.restoreBackup = function (backup, opts) { + return this.base.get(this.url + "backup/restore/" + backup, opts); + }; + + OctoKlipperClient.prototype.restoreBackupFromUpload = function (file, data) { + data = data || {}; + + var filename = data.filename || undefined; + return this.base.upload(this.url + "restore", file, filename, data); + }; + + OctoPrintClient.registerPluginComponent("klipper", OctoKlipperClient); + return OctoKlipperClient; +}); diff --git a/octoprint_klipper/static/css/klipper.css b/octoprint_klipper/static/css/klipper.css index ddf2596..48dd006 100644 --- a/octoprint_klipper/static/css/klipper.css +++ b/octoprint_klipper/static/css/klipper.css @@ -1,258 +1,383 @@ .plugin-klipper-sidebar { - padding: 1px; - height: auto; - border: 1px solid #aaa; - width: 98%; - text-align: center; - word-break: break-all; - margin: auto; + padding: 1px; + height: auto; + border: 1px solid #aaa; + width: 98%; + text-align: center; + word-break: break-all; + margin: auto; } li#navbar_plugin_klipper { - cursor: pointer; - max-width:360px; - max-height:80px; - word-break: break-all; + cursor: pointer; + max-width: 360px; + max-height: 80px; + word-break: break-all; } .plugin-klipper-sidebar a { - padding: 2px 2px; - text-align: center; - text-decoration: none; - display: inline-block; - cursor: pointer; + padding: 2px 2px; + text-align: center; + text-decoration: none; + display: inline-block; + cursor: pointer; } -.plugin-klipper-sidebar a:hover, .plugin-klipper-sidebar a:active { - cursor: pointer; - } +.plugin-klipper-sidebar a:hover, +.plugin-klipper-sidebar a:active { + cursor: pointer; +} .plugin-klipper-log { - padding: 0px; - overflow-y: scroll; - height: 400px; - border: 1px solid #eee; - width: 100%; - word-break: break-all; + padding: 0px; + overflow-y: scroll; + height: 400px; + border: 1px solid #eee; + width: 100%; + word-break: break-all; } .plugin-klipper-log .log-item { - margin: 3px auto 0 auto; - border: 1px solid #ddd; - border-radius: 3px; - background-color: #efefef; - color: #333; + margin: 3px auto 0 auto; + border: 1px solid #ddd; + border-radius: 3px; + background-color: #efefef; + color: #333; } .plugin-klipper-log .error { - background-color: #eebabb; + background-color: #eebabb; } .plugin-klipper-log .log-item .ts { - display: inline-block; - width: 13%; - height: 100%; - vertical-align: top; - font-size: 0.8em; - padding: 0 0 0 5px; + display: inline-block; + width: 13%; + height: 100%; + vertical-align: top; + font-size: 0.8em; + padding: 0 0 0 5px; } .plugin-klipper-log .log-item .msg { - display: inline-block; - width: 84%; - height: 100%; + display: inline-block; + width: 84%; + height: 100%; } .clear-btn { - margin-top: 6px; - margin-bottom: 6px; + margin-top: 6px; + margin-bottom: 6px; } #level .controls { - padding: 1px; + padding: 1px; } ul#klipper-settings { - margin: 0; + margin: 0; } -#klipper-settings a{ - margin: 5px; +#klipper-settings a { + margin: 5px; } #tab_plugin_klipper_main .row-fluid { - display: flex; - flex: row wrap; - align-items: stretch; + display: flex; + flex-flow: row wrap; + align-items: stretch; } @media all and (max-width: 940px) { - #tab_plugin_klipper_main .row-fluid { - /* On small screens, we are no longer using row direction but column */ - flex-direction: column; - } - } + #tab_plugin_klipper_main .row-fluid { + /* On small screens, we are no longer using row direction but column */ + flex-direction: column; + } +} #tab_plugin_klipper_main #left-side { - flex: 3 1; - padding-right: 10px; + flex: 3 1; + padding-right: 10px; } #tab_plugin_klipper_main .span8 label { - float: left; + float: left; } #tab_plugin_klipper_main #right-side { - flex: 1 1; - max-width: 200px; - min-width: 100px; + flex: 1 1; + max-width: 200px; + min-width: 100px; +} + +.klipper-row-fluid { + display: flex; + flex-flow: row wrap; + align-items: stretch; +} + +.klipper-column-fluid { + display: flex; + flex-flow: column nowrap; + align-items: stretch; +} + +.klipper-fluid-item-1 { + flex: 1 auto; +} + +.klipper-fluid-item-2 { + flex: 2 auto; +} + +.klipper-fluid-item-3 { + flex: 3 auto; +} + +.gap { + justify-content: space-evenly; +} + +@media all and (max-width: 940px) { + .klipper-row-fluid { + /* On small screens, we are no longer using row direction but column */ + flex-direction: column; + } } #settings_plugin_klipper { - height: 100%; - height: -webkit-fill-available; + height: 100%; + height: -webkit-fill-available; +} + +div#klipper_backups_dialog div.modal-body textarea { + margin-bottom: 0px !important; + padding-left: 0px !important; + padding-right: 0px !important; + box-sizing: border-box; + height: -webkit-fill-available; + resize: none; + width: 100%; } /* UIcustomizer fix */ - body.UICResponsiveMode #settings_dialog_content { - height: calc(100% - 60px); - margin-right: -18px; - margin-top: 50px; - width: calc(100% - 15px); + height: calc(100% - 60px); + margin-right: -18px; + margin-top: 50px; + width: calc(100% - 15px); } div#settings_plugin_klipper form { - margin: 0px; - height: 100%; + margin: 0px; + height: 100%; } div#settings_plugin_klipper div.tab-content { - height: calc(100% - 58px); - overflow: auto; + height: calc(100% - 58px); + overflow: auto; } div#settings_plugin_klipper div.tab-footer { - height: 20px; - width: 100%; - top: 10px; - position: relative; - border-top: 1px solid #eee; - padding-top: 3px; + height: 20px; + width: 100%; + top: 10px; + position: relative; + border-top: 1px solid #eee; + padding-top: 3px; } div#settings_plugin_klipper div.tab-content div#conf.tab-pane { - height: 100%; - width: 100%; + height: 100%; + min-height: 200px; + width: 100%; } div#settings_plugin_klipper div.tab-content div#conf.tab-pane div.control-group { - height: 100%; - margin: 0; - display: flex; - flex-direction: column; + height: 100%; + margin: 0; + display: flex; + flex-direction: column; } -div#settings_plugin_klipper div.tab-content div#conf.tab-pane div.control-group div.editor-controls{ - margin-bottom: 0px; - height: 26px; +.klipper-settings-tab { + height: 100%; + width: 100%; } -div#settings_plugin_klipper div.tab-content div#conf.tab-pane div.control-group div.conf-editor { - height: 95%; - height: calc(100% - 28px); - width: 99%; - width: calc(100% - 4px); - margin-top: 5px; - flex: 1 1; - overflow: auto; +#settings_plugin_klipper .m-0 { + margin: 0; } -div#settings_plugin_klipper div.tab-content div#conf.tab-pane div.control-group div.conf-editor div#plugin-klipper-config { - font-family: monospace; - overflow: auto; - height: 100%; - height: -webkit-fill-available; +#settings_plugin_klipper .scroll-y { + overflow-y: scroll; } -div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content div#conf.tab-pane.active button.btn.btn-small { +#settings_plugin_klipper table { + table-layout: unset !important; +} + +#settings_plugin_klipper .table-fixed thead th { + position: sticky; + top: 0; + background-color: rgba(12, 5, 5, 0.85); + color: rgb(200, 200, 200); +} + +#settings_plugin_klipper .pagination-mini { + margin: 5; +} + +div#klipper_editor { + height: 80%; + display: flex; + flex-flow: column; +} + +@media (max-width: 979px) { + + div#klipper_editor.modal { + height: unset !important; + } + +} + +div#klipper_editor .modal-header { + height: 6px; +} + +div#klipper_editor .modal-body { + overflow: auto; + min-height: 300px; + display: flex; + flex-flow: column; + flex-grow: 1; +} + +.klipper-btn-group { + display: inline-block; +} + +div#klipper_editor .modal-body label.checkbox { + display: unset; +} + +div#klipper_editor .modal-body input[type="text"] { + margin-bottom: 0px !important; +} + +div#klipper_editor .modal-body input[type="checkbox"] { + float: unset; + margin-left: unset; + vertical-align: unset; +} + +div#klipper_editor .modal-body .editor-controls { + flex: 0 auto 50px; +} + +div#klipper_editor .flex-end { + align-self: flex-end; +} + +div#klipper_editor div.conf-editor { + width: 99%; + width: calc(100% - 4px); + margin-top: 5px; + overflow: auto; + flex-grow : 1; +} + +div#klipper_editor div.conf-editor div#plugin-klipper-config { + font-family: monospace; + overflow: auto; +} + +.ace_editor { + margin: auto; + height: 200px; + width: 100%; +} + +/* div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content div#conf.tab-pane.active button.btn.btn-small ,div#klipper_editor button.btn.btn-small{ width: 30%; display: inline-block; margin: 0px 2px 2px 2px; -} +} */ /*checkboxes*/ -div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content div.tab-pane.active input.inline-checkbox { - vertical-align: -0.2em; +div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content div.tab-pane.active input.inline-checkbox, +div#klipper_editor input.inline-checkbox { + vertical-align: -0.2em; } -div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content div.tab-pane.active label.inline { - display: inline; +div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content div.tab-pane.active label.inline, +div#klipper_editor .inline { + display: inline; } div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content div.tab-pane.active div.controls input.controls-checkbox { - margin-top: 8px; + margin-top: 8px; } /*macros*/ div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content div#macros.tab-pane.active div div#item.control-group label.control-label { - width: 80px; + width: 80px; } #macros #item.control-group { - margin-bottom: 2px; - border: 2px solid #ccc; - border-radius: 3px; - background-color: #eeeeee; - color: #333; - padding-bottom: 2px; - padding-top: 2px; + margin-bottom: 2px; + border: 2px solid #ccc; + border-radius: 3px; + background-color: #eeeeee; + color: #333; + padding-bottom: 2px; + padding-top: 2px; } #klipper_graph_dialog { - width: 1050px; + width: 90%; } -#klipper_graph_dialog .full-sized-box{ - width: 1000px; - margin: 0 auto; +#klipper_graph_dialog .full-sized-box { + width: 100%; + margin: 0 auto; } #klipper_graph_dialog form { - margin: 0; + margin: 0; } #klipper_graph_dialog select { - width: auto; + width: auto; } #klipper_graph_dialog .graph-footer { - bottom:0; + bottom: 0; } #klipper_graph_dialog input { - display: inline-block; + display: inline-block; } #klipper_graph_dialog .status-label { - display: block; - position: absolute; - margin: 5px 0 0 10px; + display: block; + position: absolute; + margin: 5px 0 0 10px; } #klipper_graph_dialog .fill-checkbox { - display: block; - position: absolute; - top: 0%; - left: 50%; + display: block; + position: absolute; + top: 0%; + left: 50%; } #klipper_graph_dialog .help-inline { - display: block; - position: absolute; - top: 0px; + display: block; + position: absolute; + top: 0px; } #klipper_graph_canvas { - margin-top: 15px; + margin-top: 15px; } diff --git a/octoprint_klipper/static/js/klipper.js b/octoprint_klipper/static/js/klipper.js index 7fdfeb5..bfe75a3 100644 --- a/octoprint_klipper/static/js/klipper.js +++ b/octoprint_klipper/static/js/klipper.js @@ -14,251 +14,236 @@ // along with this program. If not, see . $(function () { - function KlipperViewModel(parameters) { - var self = this; + function KlipperViewModel(parameters) { + var self = this; - self.header = OctoPrint.getRequestHeaders({ - "content-type": "application/json", - "cache-control": "no-cache" - }); - - self.apiUrl = OctoPrint.getSimpleApiUrl("klipper"); - - self.settings = parameters[0]; - self.loginState = parameters[1]; - self.connectionState = parameters[2]; - self.levelingViewModel = parameters[3]; - self.paramMacroViewModel = parameters[4]; - self.access = parameters[5]; - - self.shortStatus_navbar = ko.observable(); - self.shortStatus_sidebar = ko.observable(); - self.logMessages = ko.observableArray(); - - self.showPopUp = function(popupType, popupTitle, message){ - var title = popupType.toUpperCase() + ": " + popupTitle; - new PNotify({ - title: title, - text: message, - type: popupType, - hide: false - }); - }; - - self.onSettingsShown = function () { - self.reloadConfig(); - } - - self.showLevelingDialog = function () { - var dialog = $("#klipper_leveling_dialog"); - dialog.modal({ - show: "true", - backdrop: "static", - keyboard: false - }); - self.levelingViewModel.initView(); - }; - - self.showPidTuningDialog = function () { - var dialog = $("#klipper_pid_tuning_dialog"); - dialog.modal({ - show: "true", - backdrop: "static", - keyboard: false - }); - }; - - self.showOffsetDialog = function () { - var dialog = $("#klipper_offset_dialog"); - dialog.modal({ - show: "true", - backdrop: "static" - }); - }; - - self.showGraphDialog = function () { - var dialog = $("#klipper_graph_dialog"); - dialog.modal({ - show: "true", - minHeight: "500px", - maxHeight: "600px" - }); - }; - - self.executeMacro = function (macro) { - var paramObjRegex = /{(.*?)}/g; - - if (!self.hasRight("MACRO")) return; - - if (macro.macro().match(paramObjRegex) == null) { - OctoPrint.control.sendGcode( - // Use .split to create an array of strings which is sent to - // OctoPrint.control.sendGcode instead of a single string. - macro.macro().split(/\r\n|\r|\n/) - ); - } else { - self.paramMacroViewModel.process(macro); - - var dialog = $("#klipper_macro_dialog"); - dialog.modal({ - show: "true", - backdrop: "static" - }); - } - }; - - self.navbarClicked = function () { - $("#tab_plugin_klipper_main_link").find("a").click(); - }; - - self.onGetStatus = function () { - OctoPrint.control.sendGcode("Status"); - }; - - self.onRestartFirmware = function () { - OctoPrint.control.sendGcode("FIRMWARE_RESTART"); - }; - - self.onRestartHost = function () { - OctoPrint.control.sendGcode("RESTART"); - }; - - self.onAfterBinding = function () { - self.connectionState.selectedPort( - self.settings.settings.plugins.klipper.connection.port() - ); - }; - - self.onDataUpdaterPluginMessage = function(plugin, data) { - if(plugin == "klipper") { - switch(data.type) { - case "PopUp": - self.showPopUp(data.subtype, data.title, data.payload); - break; - case "reload": - break; - case "console": - self.consoleMessage(data.subtype, data.payload); - break; - case "status": - if (data.payload.length > 36) { - var shortText = data.payload.substring(0, 31) + " [..]" - self.shortStatus_navbar(shortText); - } else { - self.shortStatus_navbar(data.payload); - } - self.shortStatus_sidebar(data.payload); - break; - default: - self.logMessage(data.time, data.subtype, data.payload); - self.consoleMessage(data.subtype, data.payload); - } - - //if ("warningPopUp" == data.type){ - // self.showPopUp(data.subtype, data.title, data.payload); - // return; - //} else if ("errorPopUp" == data.type){ - // self.showPopUp(data.subtype, data.title, data.payload); - // return; - //} else if ("reload" == data.type){ - // return; - //} else if ("console" == data.type) { - // self.consoleMessage(data.subtype, data.payload); - //} else if (data.type == "status") { - // self.shortStatus(data.payload); - //} else { - // self.logMessage(data.time, data.subtype, data.payload); - //} - } - }; - - self.logMessage = function (timestamp, type="info", message) { - if (!timestamp) { - var today = new Date(); - var timestamp = today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds(); - } - self.logMessages.push({ - time: timestamp, - type: type, - msg: message.replace(/\n/gi, "
") - }); - }; - - self.consoleMessage = function (type, message) { - if (self.settings.settings.plugins.klipper.configuration.debug_logging() === true) { - if (type == "info"){ - console.info("OctoKlipper : " + message); - } else if (type == "debug"){ - console.debug("OctoKlipper : " + message); - } else { - console.error("OctoKlipper : " + message); - } - } - return - }; - - self.reloadConfig = function() { - var settings = { - "crossDomain": true, - "url": self.apiUrl, - "method": "POST", - "headers": self.header, - "processData": false, - "dataType": "json", - "data": JSON.stringify({command: "reloadConfig"}) - } - - $.ajax(settings).done(function (response) { - self.consoleMessage( - "debug", - "Reloaded config file from Backend"); - }); - } - - self.onClearLog = function () { - self.logMessages.removeAll(); - }; - - self.isActive = function () { - return self.connectionState.isOperational(); - }; - - self.hasRight = function (right_role, type) { - var arg = eval("self.access.permissions.PLUGIN_KLIPPER_" + right_role); - - if (type == "Ko") { - return self.loginState.hasPermissionKo(arg); - } - return self.loginState.hasPermission(arg); - }; - - // OctoKlipper settings link - self.openOctoKlipperSettings = function (profile_type) { - if (!self.hasRight("CONFIG")) return; - - $("a#navbar_show_settings").click(); - $("li#settings_plugin_klipper_link a").click(); - if (profile_type) { - var query = "#klipper-settings a[data-profile-type='" + profile_type + "']"; - $(query).click(); - } - }; - } - - OCTOPRINT_VIEWMODELS.push({ - construct: KlipperViewModel, - dependencies: [ - "settingsViewModel", - "loginStateViewModel", - "connectionViewModel", - "klipperLevelingViewModel", - "klipperMacroDialogViewModel", - "accessViewModel" - ], - elements: [ - "#tab_plugin_klipper_main", - "#sidebar_plugin_klipper", - "#navbar_plugin_klipper" - ] + self.header = OctoPrint.getRequestHeaders({ + "content-type": "application/json", + "cache-control": "no-cache", }); + + self.apiUrl = OctoPrint.getSimpleApiUrl("klipper"); + self.Url = OctoPrint.getBlueprintUrl("klipper"); + + self.settings = parameters[0]; + self.loginState = parameters[1]; + self.connectionState = parameters[2]; + self.levelingViewModel = parameters[3]; + self.paramMacroViewModel = parameters[4]; + self.access = parameters[5]; + + self.shortStatus_navbar = ko.observable(); + self.shortStatus_sidebar = ko.observable(); + self.logMessages = ko.observableArray(); + + self.showPopUp = function (popupType, popupTitle, message) { + var title = popupType.toUpperCase() + ": " + popupTitle; + new PNotify({ + title: title, + text: message, + type: popupType, + hide: false, + icon: true + }); + }; + + self.showLevelingDialog = function () { + var dialog = $("#klipper_leveling_dialog"); + dialog.modal({ + show: "true", + backdrop: "static", + keyboard: false, + }); + self.levelingViewModel.initView(); + }; + + self.showPidTuningDialog = function () { + var dialog = $("#klipper_pid_tuning_dialog"); + dialog.modal({ + show: "true", + backdrop: "static", + keyboard: false, + }); + }; + + self.showOffsetDialog = function () { + var dialog = $("#klipper_offset_dialog"); + dialog.modal({ + show: "true", + backdrop: "static", + }); + }; + + self.showGraphDialog = function () { + var dialog = $("#klipper_graph_dialog"); + dialog.modal({ + show: "true", + minHeight: "500px", + maxHeight: "600px", + }); + }; + + self.executeMacro = function (macro) { + var paramObjRegex = /{(.*?)}/g; + + if (!self.hasRight("MACRO")) return; + + if (macro.macro().match(paramObjRegex) == null) { + OctoPrint.control.sendGcode( + // Use .split to create an array of strings which is sent to + // OctoPrint.control.sendGcode instead of a single string. + macro.macro().split(/\r\n|\r|\n/) + ); + } else { + self.paramMacroViewModel.process(macro); + + var dialog = $("#klipper_macro_dialog"); + dialog.modal({ + show: "true", + backdrop: "static", + }); + } + }; + + self.navbarClicked = function () { + $("#tab_plugin_klipper_main_link").find("a").click(); + }; + + self.onGetStatus = function () { + OctoPrint.control.sendGcode("Status"); + }; + + self.onRestartFirmware = function () { + OctoPrint.control.sendGcode("FIRMWARE_RESTART"); + }; + + self.onRestartHost = function () { + OctoPrint.control.sendGcode("RESTART"); + }; + + self.onAfterBinding = function () { + self.connectionState.selectedPort( + self.settings.settings.plugins.klipper.connection.port() + ); + }; + + self.onDataUpdaterPluginMessage = function (plugin, data) { + if (plugin == "klipper") { + switch (data.type) { + case "PopUp": + self.showPopUp(data.subtype, data.title, data.payload); + break; + case "reload": + break; + case "console": + self.consoleMessage(data.subtype, data.payload); + break; + case "status": + if (data.payload.length > 36) { + var shortText = data.payload.substring(0, 31) + " [..]"; + self.shortStatus_navbar(shortText); + } else { + self.shortStatus_navbar(data.payload); + } + self.shortStatus_sidebar(data.payload); + break; + default: + self.logMessage(data.time, data.subtype, data.payload); + self.consoleMessage(data.subtype, data.payload); + } + } + }; + + self.logMessage = function (timestamp, type = "info", message) { + if (!timestamp) { + var today = new Date(); + var timestamp = + today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds(); + } + self.logMessages.push({ + time: timestamp, + type: type, + msg: message.replace(/\n/gi, "
"), + }); + }; + + self.consoleMessage = function (type, message) { + if ( + self.settings.settings.plugins.klipper.configuration.debug_logging() === true + ) { + if (type == "info") { + console.info("OctoKlipper : " + message); + } else if (type == "debug") { + console.debug("OctoKlipper : " + message); + } else { + console.error("OctoKlipper : " + message); + } + } + return; + }; + + self.onClearLog = function () { + self.logMessages.removeAll(); + }; + + self.isActive = function () { + return self.connectionState.isOperational(); + }; + + self.hasRight = function (right_role) { + //if (self.loginState.isAdmin) return true; + if (right_role == "CONFIG") { + return self.loginState.hasPermission( + self.access.permissions.PLUGIN_KLIPPER_CONFIG + ); + } else if (right_role == "MACRO") { + return self.loginState.hasPermission( + self.access.permissions.PLUGIN_KLIPPER_MACRO + ); + } + }; + + self.hasRightKo = function (right_role) { + //if (self.loginState.isAdmin) return true; + if (right_role == "CONFIG") { + return self.loginState.hasPermissionKo( + self.access.permissions.PLUGIN_KLIPPER_CONFIG + ); + } else if (right_role == "MACRO") { + return self.loginState.hasPermissionKo( + self.access.permissions.PLUGIN_KLIPPER_MACRO + ); + } + }; + + // OctoKlipper settings link + self.openOctoKlipperSettings = function (profile_type) { + self.consoleMessage("debug", ": openOctoKlipperSettings :"); + if (!self.hasRight("CONFIG")) return; + self.consoleMessage("debug", ": openOctoKlipperSettings : Access okay"); + $("a#navbar_show_settings").click(); + $("li#settings_plugin_klipper_link a").click(); + if (profile_type) { + var query = "#klipper-settings a[data-profile-type='" + profile_type + "']"; + $(query).click(); + } + }; + } + + OCTOPRINT_VIEWMODELS.push({ + construct: KlipperViewModel, + dependencies: [ + "settingsViewModel", + "loginStateViewModel", + "connectionViewModel", + "klipperLevelingViewModel", + "klipperMacroDialogViewModel", + "accessViewModel", + ], + elements: [ + "#tab_plugin_klipper_main", + "#sidebar_plugin_klipper", + "#navbar_plugin_klipper", + ], + }); }); diff --git a/octoprint_klipper/static/js/klipper_backup.js b/octoprint_klipper/static/js/klipper_backup.js new file mode 100644 index 0000000..c495147 --- /dev/null +++ b/octoprint_klipper/static/js/klipper_backup.js @@ -0,0 +1,297 @@ +// + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +$(function () { + function KlipperBackupViewModel(parameters) { + var self = this; + + self.loginState = parameters[0]; + self.klipperViewModel = parameters[1]; + self.access = parameters[2]; + + self.header = OctoPrint.getRequestHeaders({ + "content-type": "application/json", + "cache-control": "no-cache", + }); + + self.apiUrl = OctoPrint.getSimpleApiUrl("klipper"); + self.Url = OctoPrint.getBlueprintUrl("klipper"); + + self.markedForFileRestore = ko.observableArray([]); + + self.CfgContent = ko.observable(); + + //uploads + self.maxUploadSize = ko.observable(0); + self.backupUploadData = undefined; + self.backupUploadName = ko.observable(); + self.isAboveUploadSize = function (data) { + return data.size > self.maxUploadSize(); + }; + + self.onStartupComplete = function () { + if (self.loginState.loggedIn()) { + self.listBakFiles(); + } + }; + + // initialize list helper + self.backups = new ItemListHelper( + "klipperBakFiles", + { + name: function (a, b) { + // sorts ascending + if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase()) return -1; + if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase()) return 1; + return 0; + }, + date: function (a, b) { + // sorts descending + if (a["date"] > b["date"]) return -1; + if (a["date"] < b["date"]) return 1; + return 0; + }, + size: function (a, b) { + // sorts descending + if (a["bytes"] > b["bytes"]) return -1; + if (a["bytes"] < b["bytes"]) return 1; + return 0; + }, + }, + {}, + "name", + [], + [], + 5 + ); + + self.listBakFiles = function () { + self.klipperViewModel.consoleMessage("debug", "listBakFiles:"); + + OctoPrint.plugins.klipper.listCfgBak() + .done(function (response) { + self.klipperViewModel.consoleMessage("debug", "listBakFilesdone: " + response); + self.backups.updateItems(response.files); + self.backups.resetPage(); + }); + }; + + self.showCfg = function (backup) { + if (!self.loginState.hasPermission(self.access.permissions.PLUGIN_KLIPPER_CONFIG)) return; + + OctoPrint.plugins.klipper.getCfgBak(backup).done(function (response) { + $('#klipper_backups_dialog textarea').attr('rows', response.response.config.split(/\r\n|\r|\n/).length); + self.CfgContent(response.response.config); + }); + }; + + self.removeCfg = function (backup) { + if (!self.loginState.hasPermission(self.access.permissions.PLUGIN_KLIPPER_CONFIG)) return; + + var perform = function () { + OctoPrint.plugins.klipper + .deleteBackup(backup) + .done(function () { + self.listBakFiles(); + }) + .fail(function (response) { + var html = "

" + _.sprintf(gettext("Failed to remove config %(name)s.

Please consult octoprint.log for details.

"), { name: _.escape(backup) }); + html += pnotifyAdditionalInfo('
' + _.escape(response.responseText) + "
"); + new PNotify({ + title: gettext("Could not remove config"), + text: html, + type: "error", + hide: false, + }); + }); + }; + + showConfirmationDialog( + _.sprintf(gettext('You are about to delete backuped config file "%(name)s".'), { + name: _.escape(backup), + }), + perform + ); + }; + + self.restoreBak = function (backup) { + if (!self.loginState.hasPermission(self.access.permissions.PLUGIN_KLIPPER_CONFIG)) return; + + var restore = function () { + OctoPrint.plugins.klipper.restoreBackup(backup).done(function (response) { + self.klipperViewModel.consoleMessage("debug", "restoreCfg: " + response.text); + }); + }; + + var html = "

" + gettext("This will overwrite any file with the same name on the configpath.") + "

" + "

" + backup + "

"; + + showConfirmationDialog({ + title: gettext("Are you sure you want to restore now?"), + html: html, + proceed: gettext("Proceed"), + onproceed: restore(), + }); + }; + + self.markFilesOnPage = function () { + self.markedForFileRestore(_.uniq(self.markedForFileRestore().concat(_.map(self.backups.paginatedItems(), "file")))); + }; + + self.markAllFiles = function () { + self.markedForFileRestore(_.map(self.backups.allItems, "file")); + }; + + self.clearMarkedFiles = function () { + self.markedForFileRestore.removeAll(); + }; + + self.restoreMarkedFiles = function () { + var perform = function () { + self._bulkRestore(self.markedForFileRestore()).done(function () { + self.markedForFileRestore.removeAll(); + }); + }; + + showConfirmationDialog( + _.sprintf(gettext("You are about to restore %(count)d backuped config files."), { + count: self.markedForFileRestore().length, + }), + perform + ); + }; + + self.removeMarkedFiles = function () { + var perform = function () { + self._bulkRemove(self.markedForFileRestore()).done(function () { + self.markedForFileRestore.removeAll(); + }); + }; + + showConfirmationDialog( + _.sprintf(gettext("You are about to delete %(count)d backuped config files."), { + count: self.markedForFileRestore().length, + }), + perform + ); + }; + + self._bulkRestore = function (files) { + var title, message, handler; + + title = gettext("Restoring klipper config files"); + self.klipperViewModel.consoleMessage("debug", title); + message = _.sprintf(gettext("Restoring %(count)d backuped config files..."), { + count: files.length, + }); + + handler = function (filename) { + return OctoPrint.plugins.klipper + .restoreBackup(filename) + .done(function (response) { + deferred.notify( + _.sprintf(gettext("Restored %(filename)s..."), { + filename: _.escape(filename), + }), + true + ); + self.klipperViewModel.consoleMessage("debug", "restoreCfg: " + response.text); + self.markedForFileRestore.remove(function (item) { + return item.name == filename; + }); + }) + .fail(function () { + deferred.notify(_.sprintf(gettext("Restoring of %(filename)s failed, continuing..."), { filename: _.escape(filename) }), false); + }); + }; + + var deferred = $.Deferred(); + + var promise = deferred.promise(); + + var options = { + title: title, + message: message, + max: files.length, + output: true, + }; + showProgressModal(options, promise); + + var requests = []; + + _.each(files, function (filename) { + var request = handler(filename); + requests.push(request); + }); + + $.when.apply($, _.map(requests, wrapPromiseWithAlways)).done(function () { + deferred.resolve(); + }); + + return promise; + }; + + self._bulkRemove = function (files) { + var title, message, handler; + + title = gettext("Deleting backup files"); + message = _.sprintf(gettext("Deleting %(count)d backup files..."), { + count: files.length, + }); + + handler = function (filename) { + return OctoPrint.plugins.klipper + .deleteBackup(filename) + .done(function () { + deferred.notify(_.sprintf(gettext("Deleted %(filename)s..."), { filename: _.escape(filename) }), true); + self.markedForFileRestore.remove(function (item) { + return item.name == filename; + }); + }) + .fail(function () { + deferred.notify(_.sprintf(gettext("Deleting of %(filename)s failed, continuing..."), { filename: _.escape(filename) }), false); + }); + }; + + var deferred = $.Deferred(); + var promise = deferred.promise(); + var options = { + title: title, + message: message, + max: files.length, + output: true, + }; + showProgressModal(options, promise); + + var requests = []; + _.each(files, function (filename) { + var request = handler(filename); + requests.push(request); + }); + + $.when.apply($, _.map(requests, wrapPromiseWithAlways)).done(function () { + deferred.resolve(); + self.listBakFiles(); + }); + + return promise; + }; + } + + OCTOPRINT_VIEWMODELS.push({ + construct: KlipperBackupViewModel, + dependencies: ["loginStateViewModel", "klipperViewModel", "accessViewModel"], + elements: ["#klipper_backups_dialog"], + }); +}); diff --git a/octoprint_klipper/static/js/klipper_editor.js b/octoprint_klipper/static/js/klipper_editor.js new file mode 100644 index 0000000..c8867a9 --- /dev/null +++ b/octoprint_klipper/static/js/klipper_editor.js @@ -0,0 +1,234 @@ +// + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +$(function () { + function KlipperEditorViewModel(parameters) { + var self = this; + var obKlipperConfig = null; + var editor = null; + + self.settings = parameters[0]; + self.klipperViewModel = parameters[1]; + + self.CfgFilename = ko.observable(""); + self.CfgContent = ko.observable(""); + self.config = [] + + self.header = OctoPrint.getRequestHeaders({ + "content-type": "application/json", + "cache-control": "no-cache", + }); + + self.process = function (config) { + self.config = config; + self.CfgFilename(config.file); + self.CfgContent(config.content); + + if (editor) { + editor.session.setValue(config.content); + editor.clearSelection(); + } + setInterval(function () { + if (editor) { + + + var modalbodyheight = $('#klipper_editor').height(); + //$('#conf_editor').height( modalbodyheight - 135 ); + editor.resize(); + + }; + }, 500); + }; + + self.checkSyntax = function () { + if (editor.session) { + self.klipperViewModel.consoleMessage("debug", "checkSyntax:"); + + OctoPrint.plugins.klipper.checkCfg(editor.session.getValue()) + .done(function (response) { + var msg = "" + if (response.is_syntax_ok == true) { + msg = gettext('Syntax OK') + } else { + msg = gettext('Syntax NOK') + } + showMessageDialog( + msg, + { + title: gettext("SyntaxCheck") + } + ) + }); + }; + }; + + self.saveCfg = function () { + if (editor.session) { + self.klipperViewModel.consoleMessage("debug", "Save:"); + + OctoPrint.plugins.klipper.saveCfg(editor.session.getValue(), self.CfgFilename()) + .done(function (response) { + var msg = "" + if (response.saved === true) { + msg = gettext('File saved.') + } else { + msg = gettext('File not saved.') + } + showMessageDialog( + msg, + { + title: gettext("Save File") + } + ) + }); + } + }; + + self.minusFontsize = function () { + self.settings.settings.plugins.klipper.configuration.fontsize( + self.settings.settings.plugins.klipper.configuration.fontsize() - 1 + ); + if (self.settings.settings.plugins.klipper.configuration.fontsize() < 9) { + self.settings.settings.plugins.klipper.configuration.fontsize(9); + } + if (editor) { + editor.setFontSize( + self.settings.settings.plugins.klipper.configuration.fontsize() + ); + editor.resize(); + } + }; + + self.plusFontsize = function () { + self.settings.settings.plugins.klipper.configuration.fontsize( + self.settings.settings.plugins.klipper.configuration.fontsize() + 1 + ); + if (self.settings.settings.plugins.klipper.configuration.fontsize() > 20) { + self.settings.settings.plugins.klipper.configuration.fontsize(20); + } + if (editor) { + editor.setFontSize( + self.settings.settings.plugins.klipper.configuration.fontsize() + ); + editor.resize(); + } + }; + + self.loadLastSession = function () { + if (self.settings.settings.plugins.klipper.configuration.temp_config() != "") { + self.klipperViewModel.consoleMessage( + "info", + "lastSession:" + + self.settings.settings.plugins.klipper.configuration.temp_config() + ); + if (editor.session) { + editor.session.setValue( + self.settings.settings.plugins.klipper.configuration.temp_config() + ); + editor.clearSelection(); + } + } + }; + + self.reloadFromFile = function () { + + OctoPrint.plugins.klipper.getCfg(self.CfgFilename()) + .done(function (response) { + self.klipperViewModel.consoleMessage("debug", "reloadFromFile: " + response); + if (response.response.text != "") { + var msg = response.response.text + showMessageDialog( + msg, + { + title: gettext("Reload File") + } + ) + } else { + if (editor) { + editor.session.setValue(response.response.config); + editor.clearSelection(); + } + } + }) + .fail(function (response) { + var msg = response + showMessageDialog( + msg, + { + title: gettext("Reload File") + } + ) + }); + }; + + self.configBound = function (config) { + config.withSilence = function () { + this.notifySubscribers = function () { + if (!this.isSilent) { + ko.subscribable.fn.notifySubscribers.apply(this, arguments); + } + } + + this.silentUpdate = function (newValue) { + this.isSilent = true; + this(newValue); + this.isSilent = false; + }; + + return this; + } + + obKlipperConfig = config.withSilence(); + if (editor) { + editor.setValue(obKlipperConfig()); + editor.setFontSize(self.settings.settings.plugins.klipper.configuration.fontsize()); + editor.resize(); + editor.clearSelection(); + } + return obKlipperConfig; + }; + + self.onStartup = function () { + ace.config.set("basePath", "plugin/klipper/static/js/lib/ace/"); + editor = ace.edit("plugin-klipper-config"); + editor.setTheme("ace/theme/monokai"); + editor.session.setMode("ace/mode/klipper_config"); + editor.clearSelection(); + + editor.setOptions({ + hScrollBarAlwaysVisible: false, + vScrollBarAlwaysVisible: false, + autoScrollEditorIntoView: true, + showPrintMargin: false, + maxLines: "Infinity", + minLines: 100 + //maxLines: "Infinity" + }); + + editor.session.on('change', function (delta) { + if (obKlipperConfig) { + obKlipperConfig.silentUpdate(editor.getValue()); + editor.resize(); + } + }); + }; + } + + OCTOPRINT_VIEWMODELS.push({ + construct: KlipperEditorViewModel, + dependencies: ["settingsViewModel", "klipperViewModel"], + elements: ["#klipper_editor"], + }); +}); diff --git a/octoprint_klipper/static/js/klipper_settings.js b/octoprint_klipper/static/js/klipper_settings.js index 2f4ae69..2b7e63d 100644 --- a/octoprint_klipper/static/js/klipper_settings.js +++ b/octoprint_klipper/static/js/klipper_settings.js @@ -13,243 +13,293 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -$(function() { - $('#klipper-settings a:first').tab('show'); - function KlipperSettingsViewModel(parameters) { - var self = this; - var obKlipperConfig = null; - var editor = null; +$(function () { + $("#klipper-settings a:first").tab("show"); + function KlipperSettingsViewModel(parameters) { + var self = this; - self.settings = parameters[0]; - self.klipperViewModel = parameters[1]; + self.settings = parameters[0]; + self.klipperViewModel = parameters[1]; + self.klipperEditorViewModel = parameters[2]; + self.klipperBackupViewModel = parameters[3]; + self.access = parameters[4]; - self.header = OctoPrint.getRequestHeaders({ - "content-type": "application/json", - "cache-control": "no-cache" - }); - - self.apiUrl = OctoPrint.getSimpleApiUrl("klipper"); - - self.onSettingsBeforeSave = function () { - if (editor.session) { - self.klipperViewModel.consoleMessage("debug", "onSettingsBeforeSave:") - var settings = { - "crossDomain": true, - "url": self.apiUrl, - "method": "POST", - "headers": self.header, - "processData": false, - "dataType": "json", - "data": JSON.stringify({command: "checkConfig", - config: editor.session.getValue()}) - } - - $.ajax(settings).done(function (response) { - }); - } - } - - self.addMacro = function() { - self.settings.settings.plugins.klipper.macros.push({ - name: 'Macro', - macro: '', - sidebar: true, - tab: true - }); - } - - self.removeMacro = function(macro) { - self.settings.settings.plugins.klipper.macros.remove(macro); - } - - self.moveMacroUp = function(macro) { - self.moveItemUp(self.settings.settings.plugins.klipper.macros, macro) - } - - self.moveMacroDown = function(macro) { - self.moveItemDown(self.settings.settings.plugins.klipper.macros, macro) - } - - self.addProbePoint = function() { - self.settings.settings.plugins.klipper.probe.points.push( - { - name: 'point-#', - x:0, y:0, z:0 - } - ); - } - - self.removeProbePoint = function(point) { - self.settings.settings.plugins.klipper.probe.points.remove(point); - } - - self.moveProbePointUp = function(macro) { - self.moveItemUp(self.settings.settings.plugins.klipper.probe.points, macro) - } - - self.moveProbePointDown = function(macro) { - self.moveItemDown(self.settings.settings.plugins.klipper.probe.points, macro) - } - - self.moveItemDown = function(list, item) { - var i = list().indexOf(item); - if (i < list().length - 1) { - var rawList = list(); - list.splice(i, 2, rawList[i + 1], rawList[i]); - } - } - - self.moveItemUp = function(list, item) { - var i = list().indexOf(item); - if (i > 0) { - var rawList = list(); - list.splice(i-1, 2, rawList[i], rawList[i-1]); - } - } - - self.minusFontsize = function () { - self.settings.settings.plugins.klipper.configuration.fontsize(self.settings.settings.plugins.klipper.configuration.fontsize() - 1); - if (self.settings.settings.plugins.klipper.configuration.fontsize() < 9) { - self.settings.settings.plugins.klipper.configuration.fontsize(9); - } - if (editor) { - editor.setFontSize(self.settings.settings.plugins.klipper.configuration.fontsize()); - editor.resize(); - } - } - - self.plusFontsize = function () { - self.settings.settings.plugins.klipper.configuration.fontsize(self.settings.settings.plugins.klipper.configuration.fontsize() + 1); - if (self.settings.settings.plugins.klipper.configuration.fontsize() > 20) { - self.settings.settings.plugins.klipper.configuration.fontsize(20); - } - if (editor) { - editor.setFontSize(self.settings.settings.plugins.klipper.configuration.fontsize()); - editor.resize(); - } - } - - self.loadLastSession = function () { - if (self.settings.settings.plugins.klipper.configuration.old_config() != "") { - self.klipperViewModel.consoleMessage("info","lastSession:" + self.settings.settings.plugins.klipper.configuration.old_config()) - if (editor.session) { - editor.session.setValue(self.settings.settings.plugins.klipper.configuration.old_config()); - editor.clearSelection(); - } - } - } - - self.reloadFromFile = function () { - if (editor.session) { - var settings = { - "crossDomain": true, - "url": self.apiUrl, - "method": "POST", - "headers": self.header, - "processData": false, - "dataType": "json", - "data": JSON.stringify({command: "reloadConfig"}) - } - - $.ajax(settings).done(function (response) { - if (editor.session) { - editor.session.setValue(response["data"]); - editor.clearSelection(); - } - }); - } - } - - self.loadCfgBackup = function () { - if (editor.session) { - var settings = { - "crossDomain": true, - "url": self.apiUrl, - "method": "POST", - "headers": self.header, - "processData": false, - "dataType": "json", - "data": JSON.stringify({command: "reloadCfgBackup"}) - } - - $.ajax(settings).done(function (response) { - if (editor.session) { - editor.session.setValue(response["data"]); - editor.clearSelection(); - } - }); - } - } - - self.configBound = function (config) { - config.withSilence = function() { - this.notifySubscribers = function() { - if (!this.isSilent) { - ko.subscribable.fn.notifySubscribers.apply(this, arguments); - } - } - - this.silentUpdate = function(newValue) { - this.isSilent = true; - this(newValue); - this.isSilent = false; - }; - - return this; - } - - obKlipperConfig = config.withSilence(); - if (editor) { - editor.setValue(obKlipperConfig()); - editor.setFontSize(self.settings.settings.plugins.klipper.configuration.fontsize()); - editor.resize(); - editor.clearSelection(); - } - return obKlipperConfig; - } - - ace.config.set("basePath", "plugin/klipper/static/js/lib/ace/"); - editor = ace.edit("plugin-klipper-config"); - editor.setTheme("ace/theme/monokai"); - editor.session.setMode("ace/mode/klipper_config"); - editor.setOptions({ - hScrollBarAlwaysVisible: false, - vScrollBarAlwaysVisible: false, - autoScrollEditorIntoView: true, - showPrintMargin: false, - //maxLines: "Infinity" - }) - - editor.session.on('change', function(delta) { - if (obKlipperConfig) { - obKlipperConfig.silentUpdate(editor.getValue()); - editor.resize(); - } - }); - - // Uncomment this if not using maxLines: "Infinity"... - // setInterval(function(){ editor.resize(); }, 500); - self.onDataUpdaterPluginMessage = function(plugin, data) { - if(plugin == "klipper") { - if ("reload" == data.type){ - if ("config" == data.subtype){ - if (editor.session) { - editor.session.setValue(data.payload); - editor.clearSelection(); - } - } - return - } - } - } - } - - OCTOPRINT_VIEWMODELS.push({ - construct: KlipperSettingsViewModel, - dependencies: [ - "settingsViewModel", - "klipperViewModel" - ], - elements: ["#settings_plugin_klipper"] + self.header = OctoPrint.getRequestHeaders({ + "content-type": "application/json", + "cache-control": "no-cache", }); + + self.markedForFileRemove = ko.observableArray([]); + + // initialize list helper + self.configs = new ItemListHelper( + "klipperCfgFiles", + { + name: function (a, b) { + // sorts ascending + if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase()) return -1; + if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase()) return 1; + return 0; + }, + date: function (a, b) { + // sorts descending + if (a["date"] > b["date"]) return -1; + if (a["date"] < b["date"]) return 1; + return 0; + }, + size: function (a, b) { + // sorts descending + if (a["bytes"] > b["bytes"]) return -1; + if (a["bytes"] < b["bytes"]) return 1; + return 0; + }, + }, + {}, + "name", + [], + [], + 15 + ); + + self.onStartupComplete = function () { + self.listCfgFiles(); + }; + + self.listCfgFiles = function () { + self.klipperViewModel.consoleMessage("debug", "listCfgFiles:"); + + OctoPrint.plugins.klipper.listCfg().done(function (response) { + self.klipperViewModel.consoleMessage("debug", "listCfgFiles: " + response); + self.configs.updateItems(response.files); + self.configs.resetPage(); + }); + }; + + self.removeCfg = function (config) { + if (!self.klipperViewModel.hasRight("CONFIG")) return; + + var perform = function () { + OctoPrint.plugins.klipper + .deleteCfg(config) + .done(function () { + self.listCfgFiles(); + var html = "

" + _.sprintf(gettext("All fine

"), { name: _.escape(config) }); + new PNotify({ + title: gettext("All is fine"), + text: html, + type: "error", + hide: false, + }); + }) + .fail(function (response) { + var html = "

" + _.sprintf(gettext("Failed to remove config %(name)s.

Please consult octoprint.log for details.

"), { name: _.escape(config) }); + html += pnotifyAdditionalInfo('
' + _.escape(response.responseText) + "
"); + new PNotify({ + title: gettext("Could not remove config"), + text: html, + type: "error", + hide: false, + }); + }); + }; + + showConfirmationDialog( + _.sprintf(gettext('You are about to delete config file "%(name)s".'), { + name: _.escape(config), + }), + perform + ); + }; + + self.markFilesOnPage = function () { + self.markedForFileRemove(_.uniq(self.markedForFileRemove().concat(_.map(self.configs.paginatedItems(), "file")))); + }; + + self.markAllFiles = function () { + self.markedForFileRemove(_.map(self.configs.allItems, "file")); + }; + + self.clearMarkedFiles = function () { + self.markedForFileRemove.removeAll(); + }; + + self.removeMarkedFiles = function () { + var perform = function () { + self._bulkRemove(self.markedForFileRemove()).done(function () { + self.markedForFileRemove.removeAll(); + }); + }; + + showConfirmationDialog( + _.sprintf(gettext("You are about to delete %(count)d config files."), { + count: self.markedForFileRemove().length, + }), + perform + ); + }; + + self._bulkRemove = function (files) { + var title, message, handler; + + title = gettext("Deleting config files"); + message = _.sprintf(gettext("Deleting %(count)d config files..."), { + count: files.length, + }); + + handler = function (filename) { + return OctoPrint.plugins.klipper + .deleteCfg(filename) + .done(function () { + deferred.notify( + _.sprintf(gettext("Deleted %(filename)s..."), { + filename: _.escape(filename), + }), + true + ); + self.markedForFileRemove.remove(function (item) { + return item.name == filename; + }); + }) + .fail(function () { + deferred.notify(_.sprintf(gettext("Deleting of %(filename)s failed, continuing..."), { filename: _.escape(filename) }), false); + }); + }; + + var deferred = $.Deferred(); + var promise = deferred.promise(); + var options = { + title: title, + message: message, + max: files.length, + output: true, + }; + showProgressModal(options, promise); + + var requests = []; + _.each(files, function (filename) { + var request = handler(filename); + requests.push(request); + }); + + $.when.apply($, _.map(requests, wrapPromiseWithAlways)).done(function () { + deferred.resolve(); + self.listCfgFiles(); + }); + + return promise; + }; + + self.showBackupsDialog = function () { + self.klipperViewModel.consoleMessage("debug", "showBackupsDialog:"); + self.klipperBackupViewModel.listBakFiles(); + var dialog = $("#klipper_backups_dialog"); + dialog.modal({ + show: "true", + minHeight: "600px", + }); + }; + + self.newFile = function () { + if (!self.klipperViewModel.hasRight("CONFIG")) return; + var config = { + content: "", + file: "Change Filename", + }; + self.klipperEditorViewModel.process(config); + var editorDialog = $("#klipper_editor"); + editorDialog.modal({ + show: "true", + backdrop: "static", + }); + }; + + self.showEditUserDialog = function (file) { + if (!self.klipperViewModel.hasRight("CONFIG")) return; + + OctoPrint.plugins.klipper.getCfg(file).done(function (response) { + var config = { + content: response.response.config, + file: file, + }; + self.klipperEditorViewModel.process(config); + + var editorDialog = $("#klipper_editor"); + editorDialog.modal({ + show: "true", + backdrop: "static", + }); + }); + }; + + self.addMacro = function () { + self.settings.settings.plugins.klipper.macros.push({ + name: "Macro", + macro: "", + sidebar: true, + tab: true, + }); + }; + + self.removeMacro = function (macro) { + self.settings.settings.plugins.klipper.macros.remove(macro); + }; + + self.moveMacroUp = function (macro) { + self.moveItemUp(self.settings.settings.plugins.klipper.macros, macro); + }; + + self.moveMacroDown = function (macro) { + self.moveItemDown(self.settings.settings.plugins.klipper.macros, macro); + }; + + self.addProbePoint = function () { + self.settings.settings.plugins.klipper.probe.points.push({ + name: "point-#", + x: 0, + y: 0, + z: 0, + }); + }; + + self.removeProbePoint = function (point) { + self.settings.settings.plugins.klipper.probe.points.remove(point); + }; + + self.moveProbePointUp = function (macro) { + self.moveItemUp(self.settings.settings.plugins.klipper.probe.points, macro); + }; + + self.moveProbePointDown = function (macro) { + self.moveItemDown(self.settings.settings.plugins.klipper.probe.points, macro); + }; + + self.moveItemDown = function (list, item) { + var i = list().indexOf(item); + if (i < list().length - 1) { + var rawList = list(); + list.splice(i, 2, rawList[i + 1], rawList[i]); + } + }; + + self.moveItemUp = function (list, item) { + var i = list().indexOf(item); + if (i > 0) { + var rawList = list(); + list.splice(i - 1, 2, rawList[i], rawList[i - 1]); + } + }; + + self.onDataUpdaterPluginMessage = function (plugin, data) { + if (plugin == "klipper" && data.type == "reload" && data.subtype == "configlist") { + self.klipperViewModel.consoleMessage("debug", "onDataUpdaterPluginMessage klipper reload configlist"); + self.listCfgFiles(); + } + }; + } + + OCTOPRINT_VIEWMODELS.push({ + construct: KlipperSettingsViewModel, + dependencies: ["settingsViewModel", "klipperViewModel", "klipperEditorViewModel", "klipperBackupViewModel", "accessViewModel"], + elements: ["#settings_plugin_klipper"], + }); }); diff --git a/octoprint_klipper/templates/klipper_backups_dialog.jinja2 b/octoprint_klipper/templates/klipper_backups_dialog.jinja2 new file mode 100644 index 0000000..9c023d2 --- /dev/null +++ b/octoprint_klipper/templates/klipper_backups_dialog.jinja2 @@ -0,0 +1,92 @@ + diff --git a/octoprint_klipper/templates/klipper_editor.jinja2 b/octoprint_klipper/templates/klipper_editor.jinja2 new file mode 100644 index 0000000..bdae034 --- /dev/null +++ b/octoprint_klipper/templates/klipper_editor.jinja2 @@ -0,0 +1,45 @@ + diff --git a/octoprint_klipper/templates/klipper_graph_dialog.jinja2 b/octoprint_klipper/templates/klipper_graph_dialog.jinja2 index d24c80b..5c8b675 100644 --- a/octoprint_klipper/templates/klipper_graph_dialog.jinja2 +++ b/octoprint_klipper/templates/klipper_graph_dialog.jinja2 @@ -1,7 +1,7 @@