# coding=utf-8 __author__ = "Gina Häußge " __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' import sys import os import yaml import logging import re import uuid APPNAME="OctoPrint" instance = None def settings(init=False, configfile=None, basedir=None): global instance if instance is None: if init: instance = Settings(configfile, basedir) else: raise ValueError("Settings not initialized yet") return instance default_settings = { "serial": { "port": None, "baudrate": None, "autoconnect": False, "log": False, "timeout": { "detection": 0.5, "connection": 2, "communication": 5 } }, "server": { "host": "0.0.0.0", "port": 5000 }, "webcam": { "stream": None, "snapshot": None, "ffmpeg": None, "bitrate": "5000k", "watermark": True, "flipH": False, "flipV": False }, "feature": { "gCodeVisualizer": True, "waitForStartOnConnect": False, "alwaysSendChecksum": False, "sdSupport": True }, "folder": { "uploads": None, "timelapse": None, "timelapse_tmp": None, "logs": None, "virtualSd": None }, "temperature": { "profiles": [ {"name": "ABS", "extruder" : 210, "bed" : 100 }, {"name": "PLA", "extruder" : 180, "bed" : 60 } ] }, "printerParameters": { "movementSpeed": { "x": 6000, "y": 6000, "z": 200, "e": 300 }, "pauseTriggers": [] }, "appearance": { "name": "", "color": "default" }, "controls": [], "system": { "actions": [] }, "accessControl": { "enabled": False, "userManager": "octoprint.users.FilebasedUserManager", "userfile": None }, "events": { "systemCommandTrigger": { "enabled": False }, "gcodeCommandTrigger": { "enabled": False } }, "api": { "enabled": False, "key": ''.join('%02X' % ord(z) for z in uuid.uuid4().bytes) } } valid_boolean_trues = ["true", "yes", "y", "1"] class Settings(object): def __init__(self, configfile=None, basedir=None): self._logger = logging.getLogger(__name__) self.settings_dir = None self._config = None self._dirty = False self._init_settings_dir(basedir) if configfile is not None: self._configfile = configfile else: self._configfile = os.path.join(self.settings_dir, "config.yaml") self.load() def _init_settings_dir(self, basedir): if basedir is not None: self.settings_dir = basedir else: self.settings_dir = _resolveSettingsDir(APPNAME) def _getDefaultFolder(self, type): folder = default_settings["folder"][type] if folder is None: folder = os.path.join(self.settings_dir, type.replace("_", os.path.sep)) return folder #~~ load and save def load(self): if os.path.exists(self._configfile) and os.path.isfile(self._configfile): with open(self._configfile, "r") as f: self._config = yaml.safe_load(f) # chamged from else to handle cases where the file exists, but is empty / 0 bytes if not self._config: self._config = {} def save(self, force=False): if not self._dirty and not force: return with open(self._configfile, "wb") as configFile: yaml.safe_dump(self._config, configFile, default_flow_style=False, indent=" ", allow_unicode=True) self._dirty = False self.load() #~~ getter def get(self, path): if len(path) == 0: return None config = self._config defaults = default_settings while len(path) > 1: key = path.pop(0) if key in config.keys() and key in defaults.keys(): config = config[key] defaults = defaults[key] elif key in defaults.keys(): config = {} defaults = defaults[key] else: return None k = path.pop(0) if not isinstance(k, (list, tuple)): keys = [k] else: keys = k results = [] for key in keys: if key in config.keys(): results.append(config[key]) elif key in defaults: results.append(defaults[key]) else: results.append(None) if not isinstance(k, (list, tuple)): return results.pop() else: return results def getInt(self, path): value = self.get(path) if value is None: return None try: return int(value) except ValueError: self._logger.warn("Could not convert %r to a valid integer when getting option %r" % (value, path)) return None def getFloat(self, path): value = self.get(path) if value is None: return None try: return float(value) except ValueError: self._logger.warn("Could not convert %r to a valid integer when getting option %r" % (value, path)) return None def getBoolean(self, path): value = self.get(path) if value is None: return None if isinstance(value, bool): return value return value.lower() in valid_boolean_trues def getBaseFolder(self, type): if type not in default_settings["folder"].keys(): return None folder = self.get(["folder", type]) if folder is None: folder = self._getDefaultFolder(type) if not os.path.isdir(folder): os.makedirs(folder) return folder def getFeedbackControls(self): feedbackControls = [] for control in self.get(["controls"]): feedbackControls.extend(self._getFeedbackControls(control)) return feedbackControls def _getFeedbackControls(self, control=None): if control["type"] == "feedback_command": pattern = control["regex"] try: matcher = re.compile(pattern) return [(control["name"], matcher, control["template"])] except: # invalid regex or something like this, we'll just skip this entry pass elif control["type"] == "section": result = [] for c in control["children"]: result.extend(self._getFeedbackControls(c)) return result else: return [] def getPauseTriggers(self): triggers = { "enable": [], "disable": [], "toggle": [] } for trigger in self.get(["printerParameters", "pauseTriggers"]): try: regex = trigger["regex"] type = trigger["type"] if type in triggers.keys(): # make sure regex is valid re.compile(regex) # add to type list triggers[type].append(regex) except: # invalid regex or something like this, we'll just skip this entry pass result = {} for type in triggers.keys(): if len(triggers[type]) > 0: result[type] = re.compile("|".join(map(lambda x: "(%s)" % x, triggers[type]))) return result #~~ setter def set(self, path, value, force=False): if len(path) == 0: return config = self._config defaults = default_settings while len(path) > 1: key = path.pop(0) if key in config.keys() and key in defaults.keys(): config = config[key] defaults = defaults[key] elif key in defaults.keys(): config[key] = {} config = config[key] defaults = defaults[key] else: return key = path.pop(0) if not force and key in defaults.keys() and key in config.keys() and defaults[key] == value: del config[key] self._dirty = True elif force or (not key in config.keys() and defaults[key] != value) or (key in config.keys() and config[key] != value): if value is None: del config[key] else: config[key] = value self._dirty = True def setInt(self, path, value, force=False): if value is None: self.set(path, None, force) return try: intValue = int(value) except ValueError: self._logger.warn("Could not convert %r to a valid integer when setting option %r" % (value, path)) return self.set(path, intValue, force) def setFloat(self, path, value, force=False): if value is None: self.set(path, None, force) return try: floatValue = float(value) except ValueError: self._logger.warn("Could not convert %r to a valid integer when setting option %r" % (value, path)) return self.set(path, floatValue, force) def setBoolean(self, path, value, force=False): if value is None or isinstance(value, bool): self.set(path, value, force) elif value.lower() in valid_boolean_trues: self.set(path, True, force) else: self.set(path, False, force) def setBaseFolder(self, type, path, force=False): if type not in default_settings["folder"].keys(): return None currentPath = self.getBaseFolder(type) defaultPath = self._getDefaultFolder(type) if (path is None or path == defaultPath) and "folder" in self._config.keys() and type in self._config["folder"].keys(): del self._config["folder"][type] if not self._config["folder"]: del self._config["folder"] self._dirty = True elif (path != currentPath and path != defaultPath) or force: if not "folder" in self._config.keys(): self._config["folder"] = {} self._config["folder"][type] = path self._dirty = True def _resolveSettingsDir(applicationName): # taken from http://stackoverflow.com/questions/1084697/how-do-i-store-desktop-application-data-in-a-cross-platform-way-for-python if sys.platform == "darwin": from AppKit import NSSearchPathForDirectoriesInDomains # http://developer.apple.com/DOCUMENTATION/Cocoa/Reference/Foundation/Miscellaneous/Foundation_Functions/Reference/reference.html#//apple_ref/c/func/NSSearchPathForDirectoriesInDomains # NSApplicationSupportDirectory = 14 # NSUserDomainMask = 1 # True for expanding the tilde into a fully qualified path return os.path.join(NSSearchPathForDirectoriesInDomains(14, 1, True)[0], applicationName) elif sys.platform == "win32": return os.path.join(os.environ["APPDATA"], applicationName) else: return os.path.expanduser(os.path.join("~", "." + applicationName.lower()))