Merge branch 'feat/multipleCfg'

This commit is contained in:
thelastWallE 2021-09-05 15:28:57 +02:00
parent d70198b14b
commit b2575c55db
21 changed files with 2397 additions and 1176 deletions

View File

@ -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

View File

@ -14,16 +14,22 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
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,59 +153,17 @@ 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))
else:
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 self.key_exist(data, "configuration", "reload_command"):
if util.key_exist(data, "configuration", "reload_command"):
reload_command = os.path.expanduser(
data["configuration"]["reload_command"]
)
@ -203,8 +173,11 @@ class KlipperPlugin(
if reload_command != "manually":
# Restart klippy to reload config
self._printer.commands(reload_command)
self.log_info("Restarting Klipper.")
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
@ -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"])
@ -334,6 +307,18 @@ class KlipperPlugin(
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")
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"])
)
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")
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/<filename>", 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/<filename>", 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/<filename>", 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/<filename>", 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/<filename>", 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 <b>"+x+"</b> in Section: <b>"+y+"</b>\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 <b>"+x+"</b> in Section: <b>"+y+"</b>\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

View File

@ -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 <b>"+x+"</b> in Section: <b>"+y+"</b>\n"
+ "{}".format(str(error))
)
util.send_message(
self,
"PopUp",
"warning",
"OctoKlipper: Invalid Config\n",
"Config got not saved!\n\n"
+ "Invalid Value for <b>"+x+"</b> in Section: <b>"+y+"</b>\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

View File

@ -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)

View File

@ -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

View File

@ -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;
});

View File

@ -10,8 +10,8 @@
li#navbar_plugin_klipper {
cursor: pointer;
max-width:360px;
max-height:80px;
max-width: 360px;
max-height: 80px;
word-break: break-all;
}
@ -23,9 +23,10 @@ li#navbar_plugin_klipper {
cursor: pointer;
}
.plugin-klipper-sidebar a:hover, .plugin-klipper-sidebar a:active {
.plugin-klipper-sidebar a:hover,
.plugin-klipper-sidebar a:active {
cursor: pointer;
}
}
.plugin-klipper-log {
padding: 0px;
@ -76,13 +77,13 @@ ul#klipper-settings {
margin: 0;
}
#klipper-settings a{
#klipper-settings a {
margin: 5px;
}
#tab_plugin_klipper_main .row-fluid {
display: flex;
flex: row wrap;
flex-flow: row wrap;
align-items: stretch;
}
@ -91,7 +92,7 @@ ul#klipper-settings {
/* On small screens, we are no longer using row direction but column */
flex-direction: column;
}
}
}
#tab_plugin_klipper_main #left-side {
flex: 3 1;
@ -108,13 +109,57 @@ ul#klipper-settings {
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;
}
/* UIcustomizer fix */
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;
@ -143,6 +188,7 @@ div#settings_plugin_klipper div.tab-footer {
div#settings_plugin_klipper div.tab-content div#conf.tab-pane {
height: 100%;
min-height: 200px;
width: 100%;
}
@ -153,40 +199,119 @@ div#settings_plugin_klipper div.tab-content div#conf.tab-pane div.control-group
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);
#settings_plugin_klipper .m-0 {
margin: 0;
}
#settings_plugin_klipper .scroll-y {
overflow-y: scroll;
}
#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;
flex: 1 1;
overflow: auto;
flex-grow : 1;
}
div#settings_plugin_klipper div.tab-content div#conf.tab-pane div.control-group div.conf-editor div#plugin-klipper-config {
div#klipper_editor div.conf-editor div#plugin-klipper-config {
font-family: monospace;
overflow: auto;
height: 100%;
height: -webkit-fill-available;
}
div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content div#conf.tab-pane.active button.btn.btn-small {
.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 {
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 {
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;
}
@ -210,11 +335,11 @@ div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content
}
#klipper_graph_dialog {
width: 1050px;
width: 90%;
}
#klipper_graph_dialog .full-sized-box{
width: 1000px;
#klipper_graph_dialog .full-sized-box {
width: 100%;
margin: 0 auto;
}
@ -227,7 +352,7 @@ div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content
}
#klipper_graph_dialog .graph-footer {
bottom:0;
bottom: 0;
}
#klipper_graph_dialog input {

View File

@ -19,10 +19,11 @@ $(function () {
self.header = OctoPrint.getRequestHeaders({
"content-type": "application/json",
"cache-control": "no-cache"
"cache-control": "no-cache",
});
self.apiUrl = OctoPrint.getSimpleApiUrl("klipper");
self.Url = OctoPrint.getBlueprintUrl("klipper");
self.settings = parameters[0];
self.loginState = parameters[1];
@ -35,26 +36,23 @@ $(function () {
self.shortStatus_sidebar = ko.observable();
self.logMessages = ko.observableArray();
self.showPopUp = function(popupType, popupTitle, message){
self.showPopUp = function (popupType, popupTitle, message) {
var title = popupType.toUpperCase() + ": " + popupTitle;
new PNotify({
title: title,
text: message,
type: popupType,
hide: false
hide: false,
icon: true
});
};
self.onSettingsShown = function () {
self.reloadConfig();
}
self.showLevelingDialog = function () {
var dialog = $("#klipper_leveling_dialog");
dialog.modal({
show: "true",
backdrop: "static",
keyboard: false
keyboard: false,
});
self.levelingViewModel.initView();
};
@ -64,7 +62,7 @@ $(function () {
dialog.modal({
show: "true",
backdrop: "static",
keyboard: false
keyboard: false,
});
};
@ -72,7 +70,7 @@ $(function () {
var dialog = $("#klipper_offset_dialog");
dialog.modal({
show: "true",
backdrop: "static"
backdrop: "static",
});
};
@ -81,7 +79,7 @@ $(function () {
dialog.modal({
show: "true",
minHeight: "500px",
maxHeight: "600px"
maxHeight: "600px",
});
};
@ -102,7 +100,7 @@ $(function () {
var dialog = $("#klipper_macro_dialog");
dialog.modal({
show: "true",
backdrop: "static"
backdrop: "static",
});
}
};
@ -129,9 +127,9 @@ $(function () {
);
};
self.onDataUpdaterPluginMessage = function(plugin, data) {
if(plugin == "klipper") {
switch(data.type) {
self.onDataUpdaterPluginMessage = function (plugin, data) {
if (plugin == "klipper") {
switch (data.type) {
case "PopUp":
self.showPopUp(data.subtype, data.title, data.payload);
break;
@ -142,7 +140,7 @@ $(function () {
break;
case "status":
if (data.payload.length > 36) {
var shortText = data.payload.substring(0, 31) + " [..]"
var shortText = data.payload.substring(0, 31) + " [..]";
self.shortStatus_navbar(shortText);
} else {
self.shortStatus_navbar(data.payload);
@ -153,68 +151,37 @@ $(function () {
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) {
self.logMessage = function (timestamp, type = "info", message) {
if (!timestamp) {
var today = new Date();
var timestamp = today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds();
var timestamp =
today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds();
}
self.logMessages.push({
time: timestamp,
type: type,
msg: message.replace(/\n/gi, "<br>")
msg: message.replace(/\n/gi, "<br />"),
});
};
self.consoleMessage = function (type, message) {
if (self.settings.settings.plugins.klipper.configuration.debug_logging() === true) {
if (type == "info"){
if (
self.settings.settings.plugins.klipper.configuration.debug_logging() === true
) {
if (type == "info") {
console.info("OctoKlipper : " + message);
} else if (type == "debug"){
} else if (type == "debug") {
console.debug("OctoKlipper : " + message);
} else {
console.error("OctoKlipper : " + message);
}
}
return
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();
};
@ -223,19 +190,37 @@ $(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);
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
);
}
return self.loginState.hasPermission(arg);
};
// 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) {
@ -253,12 +238,12 @@ $(function () {
"connectionViewModel",
"klipperLevelingViewModel",
"klipperMacroDialogViewModel",
"accessViewModel"
"accessViewModel",
],
elements: [
"#tab_plugin_klipper_main",
"#sidebar_plugin_klipper",
"#navbar_plugin_klipper"
]
"#navbar_plugin_klipper",
],
});
});

View File

@ -0,0 +1,297 @@
// <Octoprint Klipper Plugin>
// 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 <https://www.gnu.org/licenses/>.
$(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 = "<p>" + _.sprintf(gettext("Failed to remove config %(name)s.</p><p>Please consult octoprint.log for details.</p>"), { name: _.escape(backup) });
html += pnotifyAdditionalInfo('<pre style="overflow: auto">' + _.escape(response.responseText) + "</pre>");
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 = "<p>" + gettext("This will overwrite any file with the same name on the configpath.") + "</p>" + "<p>" + backup + "</p>";
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"],
});
});

View File

@ -0,0 +1,234 @@
// <Octoprint Klipper Plugin>
// 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 <https://www.gnu.org/licenses/>.
$(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"],
});
});

View File

@ -13,243 +13,293 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
$(function() {
$('#klipper-settings a:first').tab('show');
$(function () {
$("#klipper-settings a:first").tab("show");
function KlipperSettingsViewModel(parameters) {
var self = this;
var obKlipperConfig = null;
var editor = null;
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"
"cache-control": "no-cache",
});
self.apiUrl = OctoPrint.getSimpleApiUrl("klipper");
self.markedForFileRemove = ko.observableArray([]);
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(
// initialize list helper
self.configs = new ItemListHelper(
"klipperCfgFiles",
{
name: 'point-#',
x:0, y:0, z:0
}
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.removeProbePoint = function(point) {
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 = "<p>" + _.sprintf(gettext("All fine</p>"), { name: _.escape(config) });
new PNotify({
title: gettext("All is fine"),
text: html,
type: "error",
hide: false,
});
})
.fail(function (response) {
var html = "<p>" + _.sprintf(gettext("Failed to remove config %(name)s.</p><p>Please consult octoprint.log for details.</p>"), { name: _.escape(config) });
html += pnotifyAdditionalInfo('<pre style="overflow: auto">' + _.escape(response.responseText) + "</pre>");
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.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.moveProbePointDown = function (macro) {
self.moveItemDown(self.settings.settings.plugins.klipper.probe.points, macro);
};
self.moveItemDown = function(list, item) {
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) {
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]);
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
}
}
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"
],
elements: ["#settings_plugin_klipper"]
dependencies: ["settingsViewModel", "klipperViewModel", "klipperEditorViewModel", "klipperBackupViewModel", "accessViewModel"],
elements: ["#settings_plugin_klipper"],
});
});

View File

@ -0,0 +1,92 @@
<div id="klipper_backups_dialog" class="modal hide fade large" tabindex="-1" role="dialog" aria-labelledby="klipper_backups_dialog_label"
aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3 id="klipper_dialog_label">{{ _('Backups') }}</h3>
</div>
<div class="modal-body klipper-column-fluid">
<div class="klipper-row-fluid">
<div class="klipper-fluid-item-2" data-bind="visible: $root.klipperViewModel.hasRightKo('CONFIG')">
<button class="btn btn-small" data-bind="click: listBakFiles" title="{{ _('Refresh file list') }}"><i class="icon-refresh"></i>
Refresh</button>
<div class="btn-group">
<button class="btn btn-small dropdown-toggle" data-toggle="dropdown"><i class="far fa-square"></i> <span class="caret"></span></button>
<ul class="dropdown-menu">
<li><a href="javascript:void(0)" data-bind="click: markFilesOnPage">{{ _('Select all on this page') }}</a>
</li>
<li><a href="javascript:void(0)" data-bind="click: markAllFiles">{{ _('Select all') }}</a></li>
<li class="divider"></li>
<li><a href="javascript:void(0)" data-bind="click: clearMarkedFiles">{{ _('Clear selection') }}</a></li>
</ul>
</div>
<button class="btn btn-small"
data-bind="click: restoreMarkedFiles, enable: markedForFileRestore().length > 0">{{ _('Restore selected') }}</button>
<button class="btn btn-small" data-bind="click: removeMarkedFiles, enable: markedForFileRestore().length > 0">{{
_('Delete selected') }}</button>
</div>
<div class="pull-right">
<div class="btn-group">
<button class="btn btn-small dropdown-toggle" data-toggle="dropdown"><i class="fas fa-wrench"></i> <span class="caret"></span></button>
<ul class="dropdown-menu dropdown-menu-right">
<li><a href="javascript:void(0)" data-bind="click: function() { backups.changeSorting('name'); }"><i class="fas fa-check"
data-bind="style: {visibility: backups.currentSorting() == 'name' ? 'visible' : 'hidden'}"></i> {{ _('Sort by name') }}
({{ _('ascending') }})</a></li>
<li><a href="javascript:void(0)" data-bind="click: function() { backups.changeSorting('date'); }"><i class="fas fa-check"
data-bind="style: {visibility: backups.currentSorting() == 'date' ? 'visible' : 'hidden'}"></i> {{ _('Sort by date') }}
({{ _('descending') }})</a></li>
<li><a href="javascript:void(0)" data-bind="click: function() { backups.changeSorting('size'); }"><i class="fas fa-check"
data-bind="style: {visibility: backups.currentSorting() == 'size' ? 'visible' : 'hidden'}"></i> {{ _('Sort by file size') }}
({{ _('descending') }})</a></li>
</ul>
</div>
</div>
</div>
<table class="table table-striped table-hover table-condensed table-hover" id="klipper_bak_files">
<thead>
<tr>
<th class="klipper_baks_checkbox"></th>
<th class="klipper_baks_name">{{ _('Name') }}</th>
<th class="klipper_baks_size">{{ _('Size') }}</th>
<th class="klipper_baks_action">{{ _('Action') }}</th>
</tr>
</thead>
<tbody data-bind="foreach: backups.paginatedItems">
<tr data-bind="attr: {title: name}">
<td class="klipper_baks_checkbox"><input type="checkbox"
data-bind="value: file, checked: $root.markedForFileRestore, invisible: !$root.klipperViewModel.hasRightKo('CONFIG')" />
</td>
<td class="klipper_baks_name" data-bind="text: name"></td>
<td class="klipper_baks_size" data-bind="text: size"></td>
<td class="klipper_baks_action">
<a href="javascript:void(0)" class="far fa-trash-alt" title="{{ _('Delete') }}"
data-bind="css: {disabled: !$root.klipperViewModel.hasRightKo('CONFIG')()}, click: function() { $parent.removeCfg($data.name); }"></a>
&nbsp;|&nbsp;
<a href="javascript:void(0)" class="fas fa-download" title="{{ _('Restore') }}"
data-bind="css: {disabled: !$root.klipperViewModel.hasRightKo('CONFIG')()}, click: function() { $parent.restoreBak($data.name); }"></a>
&nbsp;|&nbsp;
<a href="javascript:void(0)" class="fas fa-camera" title="{{ _('Preview') }}"
data-bind="css: {disabled: !$root.klipperViewModel.hasRightKo('CONFIG')()}, click: function() { $parent.showCfg($data.name); }"></a>
</td>
</tr>
</tbody>
</table>
<div class="pagination pagination-mini pagination-centered">
<ul>
<li data-bind="css: {disabled: backups.currentPage() === 0}"><a href="javascript:void(0)" data-bind="click: backups.prevPage">«</a></li>
</ul>
<ul data-bind="foreach: backups.pages">
<li data-bind="css: { active: $data.number === $root.backups.currentPage(), disabled: $data.number === -1 }"><a href="javascript:void(0)"
data-bind="text: $data.text, click: function() { $root.backups.changePage($data.number); }"></a></li>
</ul>
<ul>
<li data-bind="css: {disabled: backups.currentPage() === backups.lastPage()}"><a href="javascript:void(0)"
data-bind="click: backups.nextPage">»</a></li>
</ul>
</div>
<div class="klipper-fluid-item-3">
<textarea readonly data-bind="value: CfgContent" id="klipper_bak_text"></textarea>
</div>
</div>
<div class="modal-footer">
</div>
</div>

View File

@ -0,0 +1,45 @@
<div id="klipper_editor" class="modal hide fade large" tabindex="-1" role="dialog" aria-labelledby="klipper_editor_label" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3 id="klipper_dialog_label">{{ _('Editor') }}</h3>
</div>
<div class="modal-body">
<script src="plugin/klipper/static/js/lib/ace/ace.min.js" type="text/javascript" charset="utf-8"></script>
<script src="plugin/klipper/static/js/lib/ace/theme-monokai.min.js" type="text/javascript" charset="utf-8"></script>
<script src="plugin/klipper/static/js/lib/ace/mode-klipper_config.js" type="text/javascript"></script>
<div class="editor-controls">
<span class="control-label">Filename:</span>
<input type="text" data-bind="value: CfgFilename">
<div class="klipper-btn-group">
<button class="btn btn-small" data-bind="click: saveCfg" title="{{ _('Save Config') }}">
<i class="fas fa-save"></i> {{ _('Save Config') }}
</button>
<button class="btn btn-small" data-bind="click: loadLastSession" title="{{ _('Reload last version') }}">
<i class="fas fa-redo"></i> {{ _('Reload last version') }}
</button>
<button class="btn btn-small" data-bind="click: reloadFromFile" title="{{ _('Reload from file') }}">
<i class="fas fa-upload"></i> {{ _('Reload from file') }}
</button>
<button class="btn btn-small" data-bind="click: checkSyntax" title="{{ _('Check Syntax') }}">
<i class="fas fa-spell-check"></i> {{ _('Check Syntax') }}
</button>
</div>
<span class="pull-right">
<label class="checkbox">
<input class="inline-checkbox" type="checkbox" data-bind="checked: settings.settings.plugins.klipper.configuration.parse_check">
{{ _('Check parsing on save') }}
</label>
<a href='#' style="text-decoration: none;" data-bind="click: minusFontsize" title="{{ _('Decrease Fontsize') }}">
<i class="fas fa-search-minus"></i>
</a>
<a href='#' style="text-decoration: none;" data-bind="click: plusFontsize" title="{{ _('Increase Fontsize') }}">
<i class="fas fa-search-plus"></i>
</a>
</span>
</div>
<div class="conf-editor" id="conf_editor">
<input id="hdnLoadKlipperConfig" type="hidden" data-bind="value: configBound(CfgContent)">
<div id="plugin-klipper-config"></div>
</div>
</div>
</div>

View File

@ -1,7 +1,7 @@
<div id="klipper_graph_dialog" class="modal hide fade large" tabindex="-1" role="dialog" aria-labelledby="klipper_graph_dialog_label" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3 id="klipper_pid_tuning_dialog_label">{{ _('Performance Graph') }}</h3>
<h3 id="klipper_dialog_label">{{ _('Performance Graph') }}</h3>
</div>
<div class="modal-body">
<div class="full-sized-box">

View File

@ -1,7 +1,7 @@
<div id="klipper_offset_dialog" class="modal hide fade small" tabindex="-1" role="dialog" aria-labelledby="klipper_offset_dialog_label" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3 id="klipper_pid_tuning_dialog_label">{{ _('Coordinate Offset') }}</h3>
<h3 id="klipper_dialog_label">{{ _('Coordinate Offset') }}</h3>
</div>
<div class="modal-body">
<div class="row-fluid" style="margin-bottom: 15px">

View File

@ -1,7 +1,7 @@
<div id="klipper_macro_dialog" class="modal hide fade small" tabindex="-1" role="dialog" aria-labelledby="klipper_macro_dialog_label" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3 id="klipper_pid_tuning_dialog_label">{{ _('Run - ') }}<span data-bind="text: macroName"></span></h3>
<h3 id="klipper_dialog_label">{{ _('Run - ') }}<span data-bind="text: macroName"></span></h3>
</div>
<div class="modal-body">
<div class="control-group" data-bind="foreach: parameters">

View File

@ -1,7 +1,7 @@
<div id="klipper_pid_tuning_dialog" class="modal hide fade small" tabindex="-1" role="dialog" aria-labelledby="klipper_pid_tuning_dialog_label" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3 id="klipper_pid_tuning_dialog_label">{{ _('PID Tuning') }}</h3>
<h3 id="klipper_dialog_label">{{ _('PID Tuning') }}</h3>
</div>
<div class="modal-body">
<div class="control-group">

View File

@ -1,49 +1,53 @@
<form class="form-horizontal">
<ul class="nav nav-pills" id="klipper-settings">
<ul class="nav nav-pills" id="klipper-settings">
<li><a href="#basic" data-toggle="tab" data-profile-type="klipper-basic">{{ _('Basic') }}</a></li>
<li><a href="#macros" data-toggle="tab" data-profile-type="klipper-macros">{{ _('Macros') }}</a></li>
<li><a href="#level" data-toggle="tab" data-profile-type="klipper-bed">{{ _('Bed Leveling') }}</a></li>
<li><a href="#conf" data-toggle="tab" data-profile-type="klipper-config">{{ _('Klipper Configuration') }}</a></li>
</ul>
<div class="tab-content">
</ul>
<div class="tab-content">
<!-- Basics -->
<div class="tab-pane active" id="basic">
<div class="control-group">
<label class="control-label">{{ _('Serial Port') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: settings.settings.plugins.klipper.connection.port">
<input type="text" class="input-block-level" data-bind="value: settings.settings.plugins.klipper.connection.port" />
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Replace Connection Panel') }}</label>
<div class="controls">
<input class="controls-checkbox" title="{{ _('Replace Connection Panel') }}" type="checkbox" data-bind="checked: settings.settings.plugins.klipper.connection.replace_connection_panel">
<input class="controls-checkbox" title="{{ _('Replace Connection Panel') }}" type="checkbox"
data-bind="checked: settings.settings.plugins.klipper.connection.replace_connection_panel">
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Show Short Messages') }}</label>
<div class="controls">
<label class="checkbox" title="{{ _('on NavBar') }}"><input type="checkbox" data-bind="checked: settings.settings.plugins.klipper.configuration.shortStatus_navbar"> {{ _('on NavBar') }}</label>
<label class="checkbox" title="{{ _('on SideBar') }}"><input type="checkbox" data-bind="checked: settings.settings.plugins.klipper.configuration.shortStatus_sidebar"> {{ _('on SideBar') }}</label>
<label class="checkbox" title="{{ _('on NavBar') }}"><input type="checkbox"
data-bind="checked: settings.settings.plugins.klipper.configuration.shortStatus_navbar" /> {{ _('on NavBar') }}</label>
<label class="checkbox" title="{{ _('on SideBar') }}"><input type="checkbox"
data-bind="checked: settings.settings.plugins.klipper.configuration.shortStatus_sidebar" /> {{ _('on SideBar') }}</label>
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Enable debug logging') }}</label>
<div class="controls">
<input class="controls-checkbox" title="{{ _('Enable debug logging') }}" type="checkbox" data-bind="checked: settings.settings.plugins.klipper.configuration.debug_logging">
<input class="controls-checkbox" title="{{ _('Enable debug logging') }}" type="checkbox"
data-bind="checked: settings.settings.plugins.klipper.configuration.debug_logging" />
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Klipper Config File') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: settings.settings.plugins.klipper.configuration.configpath">
<input type="text" class="input-block-level" data-bind="value: settings.settings.plugins.klipper.configuration.configpath" />
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Klipper Log File') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: settings.settings.plugins.klipper.configuration.logpath">
<input type="text" class="input-block-level" data-bind="value: settings.settings.plugins.klipper.configuration.logpath" />
</div>
</div>
<div class="control-group">
@ -55,11 +59,19 @@
<option value="manually">Manually</option>
</select>
<span class="help-block">
{{ _('The command that is executed when the Klipper configuration changed and needs to be reloaded.<br>
{{ _('The command that is executed when the Klipper configuration changed and needs to be reloaded.<br />
Set this to "Manually" if you don\'t want to immediately restart klipper.') }}
</span>
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Config Backup') }}</label>
<div class="controls">
<button class="btn btn-small" data-bind='click: showBackupsDialog' title="{{ _('Show Backups') }}">
<i class="fas fa-upload"></i> {{ _('Show Backups') }}
</button>
</div>
</div>
</div>
<!-- Macros -->
<div class="tab-pane" id="macros">
@ -78,13 +90,13 @@
<div class="controls" style="margin-left: 82px;">
<div class="row-fluid">
<div class="span8">
<input type="text" class="input-block-level" data-bind="value: name"/>
<input type="text" class="input-block-level" data-bind="value: name" />
</div>
<div class="span1" style="margin: auto; text-align: center;">
<input title="{{ _('Klipper Tab') }}" style="margin: auto;" type="checkbox" class="input-block-level" data-bind="checked: tab"/>
<input title="{{ _('Klipper Tab') }}" style="margin: auto;" type="checkbox" class="input-block-level" data-bind="checked: tab" />
</div>
<div class="span1" style="margin: auto; text-align: center;">
<input title="{{ _('Sidebar') }}" style="margin: auto;" type="checkbox" class="input-block-level" data-bind="checked: sidebar"/>
<input title="{{ _('Sidebar') }}" style="margin: auto;" type="checkbox" class="input-block-level" data-bind="checked: sidebar" />
</div>
<div class="span2" style="margin: auto; text-align: center;">
<a href='#' style="vertical-align: bottom;" data-bind='click: $parent.moveMacroUp' class="fa fa-chevron-up"></a>
@ -111,7 +123,7 @@
</div>
<div class="control-group">
<span class="help-block">
{{ _('To show a dialog that asks for parameters you can write your macro like in the following example:') }}<br>
{{ _('To show a dialog that asks for parameters you can write your macro like in the following example:') }}<br />
</span>
</div>
<div class="control-group">
@ -127,7 +139,8 @@
<div class="tab-pane" id="level">
<div class="control-group">
<span class="help-block">
{{ _('This feature assists in manually leveling you print bed by moving the head to the defined points in sequence.<br>
{{ _('This feature assists in manually leveling you print bed by moving the head to the defined points in
sequence.<br />
If you use a piece of paper for leveling, set "Probe Height" to the paper thickness eg. "0.1".') }}
</span>
</div>
@ -135,7 +148,7 @@
<label class="control-label">{{ _('Probe Height') }}</label>
<div class="controls">
<div class="input-append">
<input type="text" class="input-block-level span3" data-bind="value: settings.settings.plugins.klipper.probe.height">
<input type="text" class="input-block-level span3" data-bind="value: settings.settings.plugins.klipper.probe.height" />
<span class="add-on">mm</span>
</div>
<span class="help-inline">{{ _('Z-height to probe at') }}</span>
@ -145,7 +158,7 @@
<label class="control-label">{{ _('Probe Lift') }}</label>
<div class="controls">
<div class="input-append">
<input type="text" class="input-block-level span3" data-bind="value: settings.settings.plugins.klipper.probe.lift">
<input type="text" class="input-block-level span3" data-bind="value: settings.settings.plugins.klipper.probe.lift" />
<span class="add-on">mm</span>
</div>
<span class="help-inline">{{ _('Lift Head by this amount before moving.') }}</span>
@ -155,7 +168,7 @@
<label class="control-label">{{ _('Probe Feedrate Z') }}</label>
<div class="controls">
<div class="input-append">
<input type="text" class="input-block-level span3" data-bind="value: settings.settings.plugins.klipper.probe.speed_z">
<input type="text" class="input-block-level span3" data-bind="value: settings.settings.plugins.klipper.probe.speed_z" />
<span class="add-on">mm/min</span>
</div>
</div>
@ -164,7 +177,7 @@
<label class="control-label">{{ _('Feedrate X/Y') }}</label>
<div class="controls">
<div class="input-append">
<input type="text" class="input-block-level span3" data-bind="value: settings.settings.plugins.klipper.probe.speed_xy">
<input type="text" class="input-block-level span3" data-bind="value: settings.settings.plugins.klipper.probe.speed_xy" />
<span class="add-on">mm/min</span>
</div>
</div>
@ -184,9 +197,9 @@
<label class="control-label" data-bind="text: $index"></label>
<div class="controls">
<div class="row-fluid">
<div class="span3"><input type="text" class="input-block-level" data-bind="value: name"></div>
<div class="span3"><input type="text" class="input-block-level" data-bind="value: x"></div>
<div class="span3"><input type="text" class="input-block-level" data-bind="value: y"></div>
<div class="span3"><input type="text" class="input-block-level" data-bind="value: name" /></div>
<div class="span3"><input type="text" class="input-block-level" data-bind="value: x" /></div>
<div class="span3"><input type="text" class="input-block-level" data-bind="value: y" /></div>
<div class="span3">
<a href='#' data-bind='click: $parent.moveProbePointUp' class="fa fa-chevron-up"></a>
<a href='#' data-bind='click: $parent.moveProbePointDown' class="fa fa-chevron-down"></a>
@ -197,40 +210,108 @@
</div>
<div class="control-group">
<div class="controls">
<a href='#' data-bind="click: addProbePoint" title="{{ _('Add Point') }}" class="fa fa-plus-circle"></a> {{ _('Add Point') }}
<a href='#' data-bind="click: addProbePoint" title="{{ _('Add Point') }}" class="fa fa-plus-circle"></a> {{_ ('Add Point') }}
</div>
</div>
</div>
<!-- Klipper Conf -->
<div class="tab-pane" id="conf">
<div class="control-group">
<script src="plugin/klipper/static/js/lib/ace/ace.min.js" type="text/javascript" charset="utf-8"></script>
<script src="plugin/klipper/static/js/lib/ace/theme-monokai.min.js" type="text/javascript" charset="utf-8"></script>
<script src="plugin/klipper/static/js/lib/ace/mode-klipper_config.js" type="text/javascript"></script>
<div class="editor-controls">
<button class="btn btn-small" data-bind="click: loadCfgBackup"
title="{{ _('Reload last version') }}">
<i class="fas fa-redo"></i> {{ _('Reload last version') }}
<div class="klipper-column-fluid klipper-settings-tab">
<h3 class="text-center m-0">{{ _('Config Files') }}</h3>
<div class="klipper-row-fluid">
<div class="klipper-fluid-item-2" data-bind="visible: $root.klipperViewModel.hasRightKo('CONFIG')">
<button class="btn btn-small" data-bind="click: newFile" title="{{ _('Add new File') }}">
<i class="far fa-file"></i> {{ _('New File') }}
</button>
<button class="btn btn-small" data-bind='click: reloadFromFile'
title="{{ _('Reload from file') }}">
<i class="fas fa-upload"></i> {{ _('Reload from file') }}
<button class="btn btn-small" data-bind="click: listCfgFiles" title="{{ _('Refresh file list') }}">
<i class="icon-refresh"></i> {{ _('Refresh Files') }}
</button>
<label class="inline"><input class="inline-checkbox" type="checkbox" data-bind="checked: settings.settings.plugins.klipper.configuration.parse_check"> {{ _('Check parsing on save') }}</label>
&nbsp;&nbsp;
<a href='#' data-bind="click: minusFontsize" title="{{ _('Decrease Fontsize') }}" class="fas fa-search-minus"></a>
<a href='#' data-bind="click: plusFontsize" title="{{ _('Increase Fontsize') }}" class="fas fa-search-plus"></a>
<div class="btn-group">
<button class="btn btn-small dropdown-toggle" data-toggle="dropdown"><i class="far fa-square"></i>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a href="javascript:void(0)" data-bind="click: markFilesOnPage">{{ _('Select all on this page') }}</a></li>
<li><a href="javascript:void(0)" data-bind="click: markAllFiles">{{ _('Select all') }}</a></li>
<li class="divider"></li>
<li><a href="javascript:void(0)" data-bind="click: clearMarkedFiles">{{ _('Clear selection') }}</a></li>
</ul>
</div>
<div class="conf-editor">
<input id="hdnLoadKlipperConfig" type="hidden" data-bind="value: configBound(settings.settings.plugins.klipper.config)" />
<div id="plugin-klipper-config"></div>
<button class="btn btn-small"
data-bind="click: removeMarkedFiles, enable: markedForFileRemove().length > 0">{{ _('Delete selected') }}</button>
</div>
<div class="pull-right">
<div class="btn-group">
<button class="btn btn-small dropdown-toggle" data-toggle="dropdown"><i class="fas fa-wrench"></i> <span class="caret"></span></button>
<ul class="dropdown-menu dropdown-menu-right">
<li><a href="javascript:void(0)" data-bind="click: function() { configs.changeSorting('name'); }"><i class="fas fa-check"
data-bind="style: {visibility: configs.currentSorting() == 'name' ? 'visible' : 'hidden'}"></i> {{ _('Sort by name') }}
({{ _('ascending') }})</a></li>
<li><a href="javascript:void(0)" data-bind="click: function() { configs.changeSorting('date'); }"><i class="fas fa-check"
data-bind="style: {visibility: configs.currentSorting() == 'date' ? 'visible' : 'hidden'}"></i> {{ _('Sort by date') }}
({{ _('descending') }})</a></li>
<li><a href="javascript:void(0)" data-bind="click: function() { configs.changeSorting('size'); }"><i class="fas fa-check"
data-bind="style: {visibility: configs.currentSorting() == 'size' ? 'visible' : 'hidden'}"></i> {{ _('Sort by file size') }}
({{ _('descending') }})</a></li>
</ul>
</div>
</div>
</div>
</div>
<div class="tab-footer">
<div class="scroll-y">
<table class="table table-striped table-hover table-condensed table-hover table-fixed" id="klipper_cfg_files">
<thead>
<tr>
<th class="klipper_cfgs_checkbox span1"></th>
<th class="klipper_cfgs_name">{{ _('Name') }}</th>
<th class="klipper_cfgs_size">{{ _('Size') }}</th>
<th class="klipper_cfgs_action">{{ _('Action') }}</th>
</tr>
</thead>
<tbody data-bind="foreach: configs.paginatedItems">
<tr data-bind="attr: {title: name}">
<td class="klipper_cfgs_checkbox">
<input type="checkbox"
data-bind="value: name, checked: $root.markedForFileRemove, invisible: !$root.klipperViewModel.hasRightKo('CONFIG')">
</td>
<td class="klipper_cfgs_name" data-bind="text: name"></td>
<td class="klipper_cfgs_size" data-bind="text: size"></td>
<td class="klipper_cfgs_action">
<a href="javascript:void(0)" class="far fa-trash-alt" title="{{ _('Delete') }}"
data-bind="css: {disabled: !$root.klipperViewModel.hasRightKo('CONFIG')()}, click: function() { $parent.removeCfg($data.name);}"></a>
&nbsp;|&nbsp;
<a href="javascript:void(0)" class="fas fa-download" title="{{ _('Download') }}"
data-bind="css: {disabled: !$root.klipperViewModel.hasRightKo('CONFIG')()}, attr: { href: ($root.klipperViewModel.hasRightKo('CONFIG')()) ? $data.url : 'javascript:void(0)'}"></a>
&nbsp;|&nbsp;
<a href="javascript:void(0)" class="fas fa-pencil-alt" title="{{ _('Edit') }}"
data-bind="css: {disabled: !$root.klipperViewModel.hasRightKo('CONFIG')()}, click: function() { $parent.showEditUserDialog($data.name);}"></a>
</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination pagination-mini pagination-centered">
<ul>
<li data-bind="css: {disabled: configs.currentPage() === 0}"><a href="javascript:void(0)" data-bind="click: configs.prevPage">«</a></li>
</ul>
<ul data-bind="foreach: configs.pages">
<li data-bind="css: {active: $data.number === $root.configs.currentPage(), disabled: $data.number === -1 }">
<a href="javascript:void(0)" data-bind="text: $data.text, click: function() { $root.configs.changePage($data.number); }"></a>
</li>
</ul>
<ul>
<li data-bind="css: {disabled: configs.currentPage() === configs.lastPage()}">
<a href="javascript:void(0)"
data-bind="click: configs.nextPage">»
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="tab-footer">
<a href="https://www.paypal.com/donate/?business=7P63W664NF8LA&item_name=OctoKlipper" class="btn btn-mini" target="_blank">
<i class="fab fa-paypal"> {{ _('Donate') }}</i>
</a>
</div>
</div>
</form>

View File

@ -3,7 +3,7 @@
<label for="connection_printers" data-bind="css: {disabled: !connectionState.isErrorOrClosed()}, enable: connectionState.isErrorOrClosed() && loginState.isUser()">{{ _('Printer Profile') }}</label>
<select id="connection_printers" data-bind="options: connectionState.printerOptions, optionsText: 'name', optionsValue: 'id', value: connectionState.selectedPrinter, css: {disabled: !connectionState.isErrorOrClosed()}, enable: connectionState.isErrorOrClosed() && loginState.isUser()"></select>
<button class="btn btn-block" data-bind="click: connectionState.connect, text: connectionState.buttonText(), enable: loginState.isUser()">{{ _('Connect') }}</button>
<button class="btn btn-block" data-bind="visible: hasRight('CONFIG', 'Ko'), click: function() {openOctoKlipperSettings('klipper-config');}">{{ _('Open Klipper config') }}</button>
<button class="btn btn-block" data-bind="visible: $root.loginState.hasPermissionKo($root.access.permissions.PLUGIN_KLIPPER_CONFIG), click: function() {openOctoKlipperSettings('klipper-config');}">{{ _('Open Klipper config') }}</button>
</div>
</div>
<!-- ko if: settings.settings.plugins.klipper.configuration.shortStatus_sidebar -->
@ -11,7 +11,7 @@
<a title="{{ _('Go to OctoKlipper Tab') }}" data-bind="text: shortStatus_sidebar, click: navbarClicked"></a>
</div>
<!-- /ko -->
<div class="control-group" data-bind="visible: hasRight('MACRO', 'Ko')">
<div class="control-group" data-bind="visible: $root.loginState.hasPermissionKo($root.access.permissions.PLUGIN_KLIPPER_MACRO)">
<div class="controls">
<label class="control-label small"><i class="icon-list-alt"></i> {{ _('Macros') }}</label>
<div data-bind="foreach: settings.settings.plugins.klipper.macros">

View File

@ -8,11 +8,7 @@
</div>
</div>
&nbsp;
<button
class="btn btn-mini pull-right clear-btn"
data-bind="click: onClearLog"
title="{{ _('Clear Log') }}"
>
<button class="btn btn-mini pull-right clear-btn" data-bind="click: onClearLog" title="{{ _('Clear Log') }}">
<i class="fa fa-trash"></i> {{ _('Clear Log') }}
</button>
</div>
@ -21,18 +17,13 @@
<div class="control-group">
<div class="controls">
<label class="control-label"></label>
<button
class="btn btn-block btn-small"
data-bind="click: onGetStatus, enable: isActive()"
title="{{ _('Query Klipper for its current status') }}"
>
<button class="btn btn-block btn-small" data-bind="click: onGetStatus, enable: isActive()"
title="{{ _('Query Klipper for its current status') }}">
<i class="fa icon-black fa-info-circle"></i> {{ _("Get Status") }}
</button>
<button
class="btn btn-block btn-small"
data-bind="visible: hasRight('CONFIG', 'Ko'), click: function() {openOctoKlipperSettings('klipper-config');}"
title="{{ _('Open the Klipper configuration file') }}"
>
<button class="btn btn-block btn-small"
data-bind="visible: $root.loginState.hasPermissionKo($root.access.permissions.PLUGIN_KLIPPER_CONFIG), click: function() {openOctoKlipperSettings('klipper-config');}"
title="{{ _('Open the Klipper configuration file') }}">
<i class="fa icon-black fa-file-code-o"></i>
{{ _("Open Klipper config") }}
</button>
@ -40,70 +31,44 @@
</div>
<div class="control-group">
<div class="controls">
<label class="control-label small"
><i class="icon-refresh"></i> {{ _("Restart") }}</label
>
<button
class="btn btn-block btn-small"
data-bind="click: onRestartHost, enable: isActive()"
title="{{ _('This will cause the host software to reload its config and perform an internal reset') }}"
>
<label class="control-label small"><i class="icon-refresh"></i> {{ _("Restart") }}</label>
<button class="btn btn-block btn-small" data-bind="click: onRestartHost, enable: isActive()"
title="{{ _('This will cause the host software to reload its config and perform an internal reset') }}">
{{ _("Host") }}
</button>
<button
class="btn btn-block btn-small"
data-bind="click: onRestartFirmware, enable: isActive()"
title="{{ _('Similar to a host restart, but also clears any error state from the micro-controller') }}"
>
<button class="btn btn-block btn-small" data-bind="click: onRestartFirmware, enable: isActive()"
title="{{ _('Similar to a host restart, but also clears any error state from the micro-controller') }}">
{{ _("Firmware") }}
</button>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="control-label"
><i class="icon-wrench"></i> {{ _("Tools") }}</label
>
<button
class="btn btn-block btn-small"
data-bind="click: showLevelingDialog, enable: isActive()"
title="{{ _('Assists in manually leveling your printbed by moving the head to a configurable set of positions in sequence.') }}"
>
<label class="control-label"><i class="icon-wrench"></i> {{ _("Tools") }}</label>
<button class="btn btn-block btn-small" data-bind="click: showLevelingDialog, enable: isActive()"
title="{{ _('Assists in manually leveling your printbed by moving the head to a configurable set of positions in sequence.') }}">
{{ _("Assisted Bed Leveling") }}
</button>
<button
class="btn btn-block btn-small"
data-bind="click: showPidTuningDialog, enable: isActive()"
title="{{ _('Determines optimal PID parameters by heat cycling the hotend/bed.') }}"
>
<button class="btn btn-block btn-small" data-bind="click: showPidTuningDialog, enable: isActive()"
title="{{ _('Determines optimal PID parameters by heat cycling the hotend/bed.') }}">
{{ _("PID Tuning") }}
</button>
<button
class="btn btn-block btn-small"
data-bind="click: showOffsetDialog, enable: isActive()"
title="{{ _('Sets a offset for subsequent GCODE coordinates.') }}"
>
<button class="btn btn-block btn-small" data-bind="click: showOffsetDialog, enable: isActive()"
title="{{ _('Sets a offset for subsequent GCODE coordinates.') }}">
{{ _("Coordinate Offset") }}
</button>
<button
class="btn btn-block btn-small"
data-bind="click: showGraphDialog"
title="{{ _('Assists in debugging performance issues by analyzing the Klipper log files.') }}"
>
<button class="btn btn-block btn-small" data-bind="click: showGraphDialog"
title="{{ _('Assists in debugging performance issues by analyzing the Klipper log files.') }}">
{{ _("Analyze Klipper Log") }}
</button>
</div>
</div>
<div class="controls" data-bind="visible: hasRight('MACRO', 'Ko')">
<label class="control-label"
><i class="icon-list-alt"></i> {{ _("Macros") }}</label
>
<div class="controls" data-bind="visible: $root.loginState.hasPermissionKo($root.access.permissions.PLUGIN_KLIPPER_MACRO)">
<label class="control-label"><i class="icon-list-alt"></i> {{ _("Macros") }}</label>
<div data-bind="foreach: settings.settings.plugins.klipper.macros">
<!-- ko if: tab -->
<button
class="btn btn-block btn-small"
data-bind="text: name, click: $parent.executeMacro, enable: $parent.isActive()"
></button>
<button class="btn btn-block btn-small" data-bind="text: name, click: $parent.executeMacro, enable: $parent.isActive()">
</button>
<!-- /ko -->
</div>
</div>

41
octoprint_klipper/util.py Normal file
View File

@ -0,0 +1,41 @@
def poll_status(self):
self._printer.commands("STATUS")
def update_status(self, type, status):
send_message(self, "status", type, status, status)
def file_exist(self, filepath):
'''
Returns if a file exists and shows PopUp if not
'''
from os import path
if not path.isfile(filepath):
send_message(self, "PopUp", "warning", "OctoKlipper Settings",
"File: <br />" + filepath + "<br /> does not exist!")
return False
else:
return True
def key_exist(dict, key1, key2):
try:
dict[key1][key2]
except KeyError:
return False
else:
return True
def send_message(self, type, subtype, title, payload):
"""
Send Message over API to FrontEnd
"""
import datetime
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
)
)