OctoPrint/octoprint/settings.py

384 lines
9.2 KiB
Python

# coding=utf-8
__author__ = "Gina Häußge <osd@foosel.net>"
__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()))