import configparser import gettext import os import logging import json import re import copy import pathlib from io import StringIO from os import path SCREEN_BLANKING_OPTIONS = [ "300", # 5 Minutes "900", # 15 Minutes "1800", # 30 Minutes "3600", # 1 Hour "7200", # 2 Hours "14400", # 4 Hours ] klipperscreendir = pathlib.Path(__file__).parent.resolve().parent class ConfigError(Exception): pass class KlipperScreenConfig: config = None configfile_name = "KlipperScreen.conf" do_not_edit_line = "#~# --- Do not edit below this line. This section is auto generated --- #~#" do_not_edit_prefix = "#~#" def __init__(self, configfile, screen=None): self.default_config_path = os.path.join(klipperscreendir, "ks_includes", "defaults.conf") self.config = configparser.ConfigParser() self.config_path = self.get_config_file_location(configfile) logging.debug("Config path location: %s" % self.config_path) self.defined_config = None try: self.config.read(self.default_config_path) if self.config_path != self.default_config_path: user_def, saved_def = self.separate_saved_config(self.config_path) self.defined_config = configparser.ConfigParser() self.defined_config.read_string(user_def) includes = [i[8:] for i in self.defined_config.sections() if i.startswith("include ")] for include in includes: self._include_config("/".join(self.config_path.split("/")[:-1]), include) for i in ['menu __main', 'menu __print', 'menu __splashscreen', 'preheat']: for j in self.defined_config.sections(): if j.startswith(i): for k in list(self.config.sections()): if k.startswith(i): del self.config[k] break self.log_config(self.defined_config) self.config.read_string(user_def) if saved_def is not None: self.config.read_string(saved_def) logging.info("====== Saved Def ======\n%s\n=======================" % saved_def) except KeyError: raise ConfigError(f"Error reading config: {self.config_path}") except Exception: logging.exception("Unknown error with config") printers = sorted([i for i in self.config.sections() if i.startswith("printer ")]) self.printers = [] for printer in printers: self.printers.append({ printer[8:]: { "moonraker_host": self.config.get(printer, "moonraker_host", fallback="127.0.0.1"), "moonraker_port": self.config.get(printer, "moonraker_port", fallback="7125"), "moonraker_api_key": self.config.get(printer, "moonraker_api_key", fallback=False) } }) if len(printers) <= 0: self.printers.append({ "Printer": { "moonraker_host": self.config.get("main", "moonraker_host", fallback="127.0.0.1"), "moonraker_port": self.config.get("main", "moonraker_port", fallback="7125"), "moonraker_api_key": self.config.get("main", "moonraker_api_key", fallback="") } }) conf_printers_debug = copy.deepcopy(self.printers) for printer in conf_printers_debug: name = list(printer)[0] item = conf_printers_debug[conf_printers_debug.index(printer)] if item[list(printer)[0]]['moonraker_api_key'] != "": item[list(printer)[0]]['moonraker_api_key'] = "redacted" logging.debug("Configured printers: %s" % json.dumps(conf_printers_debug, indent=2)) lang = self.get_main_config_option("language", None) lang = [lang] if lang is not None and lang != "default" else None logging.info("Detected language: %s" % lang) self.lang = gettext.translation('KlipperScreen', localedir='ks_includes/locales', languages=lang, fallback=True) self._create_configurable_options(screen) def _create_configurable_options(self, screen): _ = self.lang.gettext _n = self.lang.ngettext self.configurable_options = [ {"invert_x": {"section": "main", "name": _("Invert X"), "type": "binary", "value": "False"}}, {"invert_y": {"section": "main", "name": _("Invert Y"), "type": "binary", "value": "False"}}, {"invert_z": {"section": "main", "name": _("Invert Z"), "type": "binary", "value": "False"}}, {"language": {"section": "main", "name": _("Language"), "type": "dropdown", "value": "system_lang", "callback": screen.restart_warning, "options": [ {"name": _("System") + " " + _("(default)"), "value": "system_lang"} ]}}, {"move_speed": { "section": "main", "name": _("Move Speed (mm/s)"), "type": "scale", "value": "20", "range": [5, 100], "step": 1}}, {"print_sort_dir": {"section": "main", "type": None, "value": "name_asc"}}, {"print_estimate_method": { "section": "main", "name": _("Estimated Time Method"), "type": "dropdown", "value": "file", "options": [ {"name": _("File") + " " + _("(default)"), "value": "file"}, {"name": _("Duration Only"), "value": "duration"}, {"name": _("Filament Used"), "value": "filament"}, {"name": _("Slicer"), "value": "slicer"}]}}, {"screen_blanking": { "section": "main", "name": _("Screen Power Off Time"), "type": "dropdown", "value": "3600", "callback": screen.set_screenblanking_timeout, "options": [ {"name": _("Off"), "value": "off"}] }}, {"theme": { "section": "main", "name": _("Icon Theme"), "type": "dropdown", "value": "z-bolt", "callback": screen.restart_warning, "options": [ {"name": "Z-bolt" + " " + _("(default)"), "value": "z-bolt"}]}}, {"24htime": {"section": "main", "name": _("24 Hour Time"), "type": "binary", "value": "True"}}, {"side_macro_shortcut": { "section": "main", "name": _("Macro shortcut on sidebar"), "type": "binary", "value": "True", "callback": screen.toggle_macro_shortcut}}, {"font_size": { "section": "main", "name": _("Font Size"), "type": "dropdown", "value": "medium", "callback": screen.restart_warning, "options": [ {"name": _("Small"), "value": "small"}, {"name": _("Medium") + " " + _("(default)"), "value": "medium"}, {"name": _("Large"), "value": "large"}]}}, {"confirm_estop": {"section": "main", "name": "Confirm Emergency Stop", "type": "binary", "value": "False"}}, # {"": {"section": "main", "name": _(""), "type": ""}} ] lang_path = os.path.join(klipperscreendir, "ks_includes", "locales") langs = [d for d in os.listdir(lang_path) if not os.path.isfile(os.path.join(lang_path, d))] langs.sort() lang_opt = self.configurable_options[3]['language']['options'] for lang in langs: lang_opt.append({"name": lang, "value": lang}) t_path = os.path.join(klipperscreendir, 'styles') themes = [d for d in os.listdir(t_path) if (not os.path.isfile(os.path.join(t_path, d)) and d != "z-bolt")] themes.sort() theme_opt = self.configurable_options[8]['theme']['options'] for theme in themes: theme_opt.append({"name": theme, "value": theme}) index = self.configurable_options.index( [i for i in self.configurable_options if list(i)[0] == "screen_blanking"][0]) for num in SCREEN_BLANKING_OPTIONS: hour = int(int(num)/3600) if hour > 0: name = str(hour) + " " + _n("hour", "hours", hour) else: name = str(int(int(num)/60)) + " " + _("minutes") self.configurable_options[index]['screen_blanking']['options'].append({ "name": name, "value": num }) for item in self.configurable_options: name = list(item)[0] vals = item[name] if vals['section'] not in self.config.sections(): self.config.add_section(vals['section']) if name not in list(self.config[vals['section']]): self.config.set(vals['section'], name, vals['value']) def _include_config(self, dir, path): full_path = path if path[0] == "/" else "%s/%s" % (dir, path) parse_files = [] if "*" in full_path: parent_dir = "/".join(full_path.split("/")[:-1]) file = full_path.split("/")[-1] if not os.path.exists(parent_dir): logging.info("Config Error: Directory %s does not exist" % parent_dir) return files = os.listdir(parent_dir) regex = "^%s$" % file.replace('*', '.*') for file in files: if re.match(regex, file): parse_files.append(os.path.join(parent_dir, file)) else: if not os.path.exists(os.path.join(full_path)): logging.info("Config Error: %s does not exist" % full_path) return parse_files.append(full_path) logging.info("Parsing files: %s" % parse_files) for file in parse_files: config = configparser.ConfigParser() config.read(file) includes = [i[8:] for i in config.sections() if i.startswith("include ")] for include in includes: self._include_config("/".join(full_path.split("/")[:-1]), include) self.config.read(file) self.defined_config.read(file) def separate_saved_config(self, config_path): user_def = [] saved_def = None found_saved = False if not path.exists(config_path): return [None, None] with open(config_path) as file: for line in file: line = line.replace('\n', '') if line == self.do_not_edit_line: found_saved = True saved_def = [] continue if found_saved is False: user_def.append(line.replace('\n', '')) else: if line.startswith(self.do_not_edit_prefix): saved_def.append(line[(len(self.do_not_edit_prefix)+1):]) return ["\n".join(user_def), None if saved_def is None else "\n".join(saved_def)] def get_config_file_location(self, file): logging.info("Passed config file: %s" % file) if not path.exists(file): file = os.path.join(klipperscreendir, self.configfile_name) if not path.exists(file): file = os.path.expanduser("~/") + "klipper_config/%s" % (self.configfile_name) if not path.exists(file): file = self.default_config_path logging.info("Found configuration file at: %s" % file) return file def get_config(self): return self.config def get_configurable_options(self): return self.configurable_options def get_lang(self): return self.lang def get_main_config(self): return self.config['main'] def get_main_config_option(self, option, default=None): return self.config['main'].get(option, default) def get_menu_items(self, menu="__main", subsection=""): if subsection != "": subsection = subsection + " " index = "menu %s %s" % (menu, subsection) items = [i[len(index):] for i in self.config.sections() if i.startswith(index)] menu_items = [] for item in items: split = item.split() if len(split) == 1: menu_items.append(self._build_menu_item(menu, index + item)) return menu_items def get_menu_name(self, menu="__main", subsection=""): name = ("menu %s %s" % (menu, subsection)) if subsection != "" else ("menu %s" % menu) if name not in self.config: return False return self.config[name].get('name') def get_preheat_options(self): index = "preheat " items = [i[len(index):] for i in self.config.sections() if i.startswith(index)] preheat_options = {} for item in items: preheat_options[item] = self._build_preheat_item(index + item) return preheat_options def get_printer_config(self, name): if not name.startswith("printer "): name = "printer %s" % name if name not in self.config: return None return self.config[name] def get_printer_power_name(self): return self.config['settings'].get("printer_power_name", "printer") def get_printers(self): return self.printers def get_user_saved_config(self): if self.config_path != self.default_config_path: print("Get") def save_user_config_options(self): save_config = configparser.ConfigParser() for item in self.configurable_options: name = list(item)[0] opt = item[name] curval = self.config[opt['section']].get(name) if curval != opt["value"] or ( self.defined_config is not None and opt['section'] in self.defined_config.sections() and self.defined_config[opt['section']].get(name, None) not in (None, curval)): if opt['section'] not in save_config.sections(): save_config.add_section(opt['section']) save_config.set(opt['section'], name, str(curval)) macro_sections = [i for i in self.config.sections() if i.startswith("displayed_macros")] for macro_sec in macro_sections: for item in self.config.options(macro_sec): value = self.config[macro_sec].getboolean(item, fallback=True) if value is False or (self.defined_config is not None and macro_sec in self.defined_config.sections() and self.defined_config[macro_sec].getboolean(item, fallback=True) is False and self.defined_config[macro_sec].getboolean(item, fallback=True) != value): if macro_sec not in save_config.sections(): save_config.add_section(macro_sec) save_config.set(macro_sec, item, str(value)) save_output = self._build_config_string(save_config).split("\n") for i in range(len(save_output)): save_output[i] = "%s %s" % (self.do_not_edit_prefix, save_output[i]) if self.config_path == self.default_config_path: user_def = "" saved_def = None else: user_def, saved_def = self.separate_saved_config(self.config_path) extra_lb = "\n" if saved_def is not None else "" contents = "%s\n%s%s\n%s\n%s\n%s\n" % ( user_def, self.do_not_edit_line, extra_lb, self.do_not_edit_prefix, "\n".join(save_output), self.do_not_edit_prefix) if self.config_path != self.default_config_path: path = self.config_path else: path = os.path.expanduser("~/") if os.path.exists(path+"klipper_config/"): path = path + "klipper_config/KlipperScreen.conf" else: path = path + "KlipperScreen.conf" try: file = open(path, 'w') file.write(contents) file.close() except Exception: logging.error("Error writing configuration file in %s" % path) def set(self, section, name, value): self.config.set(section, name, value) def log_config(self, config): lines = [ " " "===== Config File =====", re.sub( r'(moonraker_api_key\s*=\s*\S+)', 'moonraker_api_key = [redacted]', self._build_config_string(config) ), "=======================" ] logging.info("\n".join(lines)) def _build_config_string(self, config): sfile = StringIO() config.write(sfile) sfile.seek(0) return sfile.read().strip() def _build_menu_item(self, menu, name): if name not in self.config: return False cfg = self.config[name] item = { "name": cfg.get("name"), "icon": cfg.get("icon"), "panel": cfg.get("panel", False), "method": cfg.get("method", False), "confirm": cfg.get("confirm", False), "enable": cfg.get("enable", True) } try: item["params"] = json.loads(cfg.get("params", "{}")) except Exception: logging.debug("Unable to parse parameters for [%s]" % name) item["params"] = {} return {name[(len(menu) + 6):]: item} def _build_preheat_item(self, name): if name not in self.config: return False cfg = self.config[name] item = { "extruder": cfg.getint("extruder", 0), "bed": cfg.getint("bed", 0), "heater_generic": cfg.getint("heater_generic", 0), "gcode": cfg.get("gcode", None) } return item