First experiments with a plugin system, service discovery and connectivity ensurance
This commit is contained in:
parent
85c170712a
commit
e1366ef90f
30 changed files with 1362 additions and 151 deletions
96
src/octoprint/plugin/__init__.py
Normal file
96
src/octoprint/plugin/__init__.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
import os
|
||||
|
||||
from octoprint.settings import settings
|
||||
from octoprint.plugin.core import (PluginInfo, PluginManager, Plugin)
|
||||
from octoprint.plugin.types import *
|
||||
|
||||
# singleton
|
||||
_instance = None
|
||||
|
||||
def plugin_manager(init=False, plugin_folders=None, plugin_types=None):
|
||||
global _instance
|
||||
if _instance is None:
|
||||
if init:
|
||||
if plugin_folders is None:
|
||||
plugin_folders = (settings().getBaseFolder("plugins"), os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "plugins")))
|
||||
if plugin_types is None:
|
||||
plugin_types = [StartupPlugin, TemplatePlugin, SettingsPlugin, SimpleApiPlugin, AssetPlugin]
|
||||
|
||||
_instance = PluginManager(plugin_folders, plugin_types)
|
||||
else:
|
||||
raise ValueError("Plugin Manager not initialized yet")
|
||||
return _instance
|
||||
|
||||
|
||||
def plugin_settings(plugin_key, defaults=None):
|
||||
return PluginSettings(settings(), plugin_key, defaults=defaults)
|
||||
|
||||
|
||||
class PluginSettings(object):
|
||||
def __init__(self, settings, plugin_key, defaults=None):
|
||||
self.settings = settings
|
||||
self.plugin_key = plugin_key
|
||||
|
||||
if defaults is None:
|
||||
defaults = dict()
|
||||
self.defaults = dict(plugins=dict())
|
||||
self.defaults["plugins"][plugin_key] = defaults
|
||||
|
||||
def prefix_path(path):
|
||||
return ['plugins', self.plugin_key] + path
|
||||
|
||||
def prefix_path_in_args(args, index=0):
|
||||
result = []
|
||||
if index == 0:
|
||||
result.append(prefix_path(args[0]))
|
||||
result.extend(args[1:])
|
||||
else:
|
||||
args_before = args[:index - 1]
|
||||
args_after = args[index + 1:]
|
||||
result.extend(args_before)
|
||||
result.append(prefix_path(args[index]))
|
||||
result.extend(args_after)
|
||||
return result
|
||||
|
||||
def add_defaults_to_kwargs(kwargs):
|
||||
kwargs.update(dict(defaults=self.defaults))
|
||||
return kwargs
|
||||
|
||||
self.access_methods = {
|
||||
'get': (lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
|
||||
'getInt': (lambda args,: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
|
||||
'getFloat': (lambda args,: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
|
||||
'getBoolean': (lambda args,: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
|
||||
'set': (lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
|
||||
'setInt': (lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
|
||||
'setFloat': (lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
|
||||
'setBoolean': (lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs))
|
||||
}
|
||||
|
||||
def globalGet(self, path):
|
||||
return self.settings.get(path)
|
||||
|
||||
def globalGetInt(self, path):
|
||||
return self.settings.getInt(path)
|
||||
|
||||
def globalGetFloat(self, path):
|
||||
return self.settings.getFloat(path)
|
||||
|
||||
def globalGetBoolean(self, path):
|
||||
return self.settings.getBoolean(path)
|
||||
|
||||
def __getattr__(self, item):
|
||||
if item in self.access_methods and hasattr(self.settings, item) and callable(getattr(self.settings, item)):
|
||||
orig_item = getattr(self.settings, item)
|
||||
args_mapper, kwargs_mapper = self.access_methods[item]
|
||||
|
||||
return lambda *args, **kwargs: orig_item(*args_mapper(args), **kwargs_mapper(kwargs))
|
||||
else:
|
||||
return getattr(self.settings, item)
|
||||
185
src/octoprint/plugin/core.py
Normal file
185
src/octoprint/plugin/core.py
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
|
||||
import os
|
||||
import imp
|
||||
from collections import defaultdict
|
||||
import logging
|
||||
|
||||
|
||||
class PluginInfo(object):
|
||||
|
||||
attr_name = '__plugin_name__'
|
||||
|
||||
attr_description = '__plugin_description__'
|
||||
|
||||
attr_version = '__plugin_version__'
|
||||
|
||||
attr_hooks = '__plugin_hooks__'
|
||||
|
||||
attr_implementations = '__plugin_implementations__'
|
||||
|
||||
attr_check = '__plugin_check__'
|
||||
|
||||
def __init__(self, key, location, instance):
|
||||
self.key = key
|
||||
self.location = location
|
||||
self.instance = instance
|
||||
|
||||
def __str__(self):
|
||||
return "{name} ({version})".format(name=self.name, version=self.version if self.version else "unknown")
|
||||
|
||||
def get_hook(self, hook):
|
||||
if not hook in self.hooks:
|
||||
return None
|
||||
return self.hooks[hook]
|
||||
|
||||
def get_implementations(self, *types):
|
||||
result = set()
|
||||
for implementation in self.implementations:
|
||||
matches_all = True
|
||||
for type in types:
|
||||
if not isinstance(implementation, type):
|
||||
matches_all = False
|
||||
if matches_all:
|
||||
result.add(implementation)
|
||||
return result
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._get_instance_attribute(self.__class__.attr_name, default=None)
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return self._get_instance_attribute(self.__class__.attr_description, default=None)
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
return self._get_instance_attribute(self.__class__.attr_version, default=None)
|
||||
|
||||
@property
|
||||
def hooks(self):
|
||||
return self._get_instance_attribute(self.__class__.attr_hooks, default={})
|
||||
|
||||
@property
|
||||
def implementations(self):
|
||||
return self._get_instance_attribute(self.__class__.attr_implementations, default=[])
|
||||
|
||||
@property
|
||||
def check(self):
|
||||
return self._get_instance_attribute(self.__class__.attr_check, default=lambda: True)
|
||||
|
||||
def _get_instance_attribute(self, attr, default=None):
|
||||
if not hasattr(self.instance, attr):
|
||||
return default
|
||||
return getattr(self.instance, attr)
|
||||
|
||||
|
||||
class PluginManager(object):
|
||||
|
||||
def __init__(self, plugin_folders, plugin_types, plugin_disabled_list=None):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
if plugin_disabled_list is None:
|
||||
plugin_disabled_list = []
|
||||
|
||||
self.plugin_folders = plugin_folders
|
||||
self.plugin_types = plugin_types
|
||||
self.plugin_disabled_list = plugin_disabled_list
|
||||
|
||||
self.plugins = dict()
|
||||
self.plugin_hooks = defaultdict(list)
|
||||
self.plugin_implementations = defaultdict(list)
|
||||
|
||||
self.reload_plugins()
|
||||
|
||||
def _find_plugins(self):
|
||||
plugins = dict()
|
||||
|
||||
for folder in self.plugin_folders:
|
||||
if not os.path.exists(folder):
|
||||
self.logger.warn("Plugin folder {folder} could not be found, skipping it".format(folder=folder))
|
||||
continue
|
||||
|
||||
entries = os.listdir(folder)
|
||||
for entry in entries:
|
||||
path = os.path.join(folder, entry)
|
||||
if os.path.isdir(path) and os.path.isfile(os.path.join(path, "__init__.py")):
|
||||
id = entry
|
||||
elif os.path.isfile(path) and entry.endswith(".py"):
|
||||
id = entry[:-3] # strip off the .py extension
|
||||
else:
|
||||
continue
|
||||
|
||||
if self._is_plugin_disabled(id):
|
||||
# plugin is disabled, ignore it
|
||||
continue
|
||||
|
||||
if id in plugins:
|
||||
# plugin is already defined, ignore it
|
||||
continue
|
||||
|
||||
module = imp.find_module(id, [folder])
|
||||
plugin = self._load_plugin(id, *module)
|
||||
if plugin.check():
|
||||
plugins[id] = plugin
|
||||
else:
|
||||
self.logger.warn("Plugin \"{plugin}\" did not pass check, disabling it".format(plugin=str(plugin)))
|
||||
|
||||
return plugins
|
||||
|
||||
def _load_plugin(self, id, f, filename, description):
|
||||
instance = imp.load_module(id, f, filename, description)
|
||||
return PluginInfo(id, filename, instance)
|
||||
|
||||
def _is_plugin_disabled(self, id):
|
||||
return id in self.plugin_disabled_list or id.endswith('disabled')
|
||||
|
||||
def reload_plugins(self):
|
||||
self.logger.info("Loading plugins from {folders}...".format(folders=", ".join(self.plugin_folders)))
|
||||
self.plugins = self._find_plugins()
|
||||
|
||||
for name, plugin in self.plugins.items():
|
||||
for hook, callback in plugin.hooks.items():
|
||||
self.plugin_hooks[hook].append((name, callback))
|
||||
for plugin_type in self.plugin_types:
|
||||
implementations = plugin.get_implementations(plugin_type)
|
||||
self.plugin_implementations[plugin_type] += ( (name, implementation) for implementation in implementations )
|
||||
|
||||
if len(self.plugins) <= 0:
|
||||
self.logger.info("No plugins found")
|
||||
else:
|
||||
self.logger.info("Found {count} plugin(s): {plugins}".format(count=len(self.plugins), plugins=", ".join(map(lambda x: str(x), self.plugins.values()))))
|
||||
|
||||
def get_plugin(self, name):
|
||||
if not name in self.plugins:
|
||||
return None
|
||||
return self.plugins[name].instance
|
||||
|
||||
def get_hooks(self, hook):
|
||||
if not hook in self.plugin_hooks:
|
||||
return []
|
||||
return {hook[0]: hook[1] for hook in self.plugin_hooks[hook]}
|
||||
|
||||
def get_implementations(self, *types):
|
||||
result = None
|
||||
|
||||
for t in types:
|
||||
implementations = self.plugin_implementations[t]
|
||||
if result is None:
|
||||
result = set(implementations)
|
||||
else:
|
||||
result = result.intersection(implementations)
|
||||
|
||||
if result is None:
|
||||
return set()
|
||||
return {impl[0]: impl[1] for impl in result}
|
||||
|
||||
|
||||
class Plugin(object):
|
||||
pass
|
||||
52
src/octoprint/plugin/types.py
Normal file
52
src/octoprint/plugin/types.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
|
||||
from .core import Plugin
|
||||
|
||||
|
||||
class StartupPlugin(Plugin):
|
||||
def on_startup(self, host, port):
|
||||
pass
|
||||
|
||||
|
||||
class AssetPlugin(Plugin):
|
||||
def get_asset_folder(self):
|
||||
return None
|
||||
|
||||
def get_assets(self):
|
||||
return []
|
||||
|
||||
|
||||
class TemplatePlugin(Plugin):
|
||||
def get_template_vars(self):
|
||||
return dict()
|
||||
|
||||
def get_template_folder(self):
|
||||
return None
|
||||
|
||||
|
||||
class SimpleApiPlugin(Plugin):
|
||||
def get_api_commands(self):
|
||||
return None
|
||||
|
||||
def on_api_command(self, command, data):
|
||||
return None
|
||||
|
||||
def on_api_get(self, request):
|
||||
return None
|
||||
|
||||
|
||||
class SettingsPlugin(TemplatePlugin):
|
||||
def on_settings_load(self):
|
||||
return None
|
||||
|
||||
def on_settings_save(self, data):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
123
src/octoprint/plugins/discovery/__init__.py
Normal file
123
src/octoprint/plugins/discovery/__init__.py
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import octoprint.plugin
|
||||
|
||||
default_settings = {
|
||||
"publicPort": None,
|
||||
"pathPrefix": None
|
||||
}
|
||||
s = octoprint.plugin.plugin_settings("netconnectd", defaults=default_settings)
|
||||
|
||||
|
||||
class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin):
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger("octoprint.plugins." + __name__)
|
||||
|
||||
self.octoprint_sd_ref = None
|
||||
self.http_sd_ref = None
|
||||
|
||||
##~~ TemplatePlugin API (part of SettingsPlugin)
|
||||
|
||||
def get_template_vars(self):
|
||||
return dict(
|
||||
_settings_menu_entry="Network discovery"
|
||||
)
|
||||
|
||||
def get_template_folder(self):
|
||||
return os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates")
|
||||
|
||||
#~~ StartupPlugin API
|
||||
|
||||
def on_startup(self, host, port):
|
||||
self._bonjour_register(host, port)
|
||||
|
||||
#~~ SettingsPlugin API
|
||||
|
||||
def on_settings_load(self):
|
||||
return {
|
||||
"publicPort": s.getInt(["publicPort"])
|
||||
}
|
||||
|
||||
def on_settings_save(self, data):
|
||||
if "publicPort" in data and data["publicPort"]:
|
||||
s.setInt(["publicPort"], data["publicPort"])
|
||||
|
||||
#~~ internals
|
||||
|
||||
def _bonjour_register(self, host, port):
|
||||
import pybonjour
|
||||
import octoprint._version
|
||||
|
||||
def register_callback(sd_ref, flags, error_code, name, reg_type, domain):
|
||||
if error_code == pybonjour.kDNSServiceErr_NoError:
|
||||
self.logger.info("Registered {name} for {reg_type} with domain {domain}".format(**locals()))
|
||||
|
||||
if s.getInt(["publicPort"]):
|
||||
port = s.getInt(["publicPort"])
|
||||
|
||||
prefix = s.globalGet(["server", "reverseProxy", "prefixFallback"])
|
||||
path = "/"
|
||||
if s.get(["pathPrefix"]):
|
||||
path = s.get(["pathPrefix"])
|
||||
elif prefix:
|
||||
path = prefix
|
||||
|
||||
domain = "local"
|
||||
|
||||
self.octoprint_sd_ref = pybonjour.DNSServiceRegister(
|
||||
name="OctoPrint API",
|
||||
regtype='_octoprint._tcp',
|
||||
port=port,
|
||||
domain=domain,
|
||||
txtRecord=pybonjour.TXTRecord({'version': octoprint._version.get_versions()['version'], 'path': path}),
|
||||
callBack=register_callback
|
||||
)
|
||||
pybonjour.DNSServiceProcessResult(self.octoprint_sd_ref)
|
||||
|
||||
self.http_sd_ref = pybonjour.DNSServiceRegister(
|
||||
name="octoprint",
|
||||
regtype='_http._tcp',
|
||||
port=port,
|
||||
domain=domain,
|
||||
txtRecord=pybonjour.TXTRecord({'path': path}),
|
||||
callBack=register_callback
|
||||
)
|
||||
pybonjour.DNSServiceProcessResult(self.http_sd_ref)
|
||||
|
||||
self.workstation_sd_ref = pybonjour.DNSServiceRegister(
|
||||
name="octoprint",
|
||||
regtype='_workstation._tcp',
|
||||
host="octoprint.local",
|
||||
port=9,
|
||||
domain=domain,
|
||||
callBack=register_callback
|
||||
)
|
||||
pybonjour.DNSServiceProcessResult(self.workstation_sd_ref)
|
||||
|
||||
|
||||
__plugin_name__ = "Discovery"
|
||||
__plugin_version__ = "0.1"
|
||||
__plugin_description__ = "Makes the OctoPrint instance discoverable via Bonjour/Avahi/Zeroconf"
|
||||
__plugin_implementations__ = []
|
||||
|
||||
def __plugin_check__():
|
||||
try:
|
||||
import pybonjour
|
||||
except:
|
||||
# no pybonjour available, we can't continue
|
||||
logging.getLogger("octoprint.plugins." + __name__).info("pybonjour is not installed, Discovery Plugin won't be available. Please manually install pybonjour and restart OctoPrint")
|
||||
return False
|
||||
|
||||
global __plugin_implementations__
|
||||
__plugin_implementations__ = [DiscoveryPlugin(),]
|
||||
return True
|
||||
|
||||
134
src/octoprint/plugins/netconnectd/__init__.py
Normal file
134
src/octoprint/plugins/netconnectd/__init__.py
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
|
||||
import os
|
||||
import logging
|
||||
from flask import jsonify
|
||||
|
||||
import octoprint.plugin
|
||||
|
||||
|
||||
default_settings = {
|
||||
"socket": "/var/run/netconnectd.sock"
|
||||
}
|
||||
|
||||
|
||||
s = octoprint.plugin.plugin_settings("netconnectd", defaults=default_settings)
|
||||
|
||||
|
||||
class NetconnectdSettingsPlugin(octoprint.plugin.SettingsPlugin, octoprint.plugin.SimpleApiPlugin, octoprint.plugin.AssetPlugin):
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
##~~ SettingsPlugin
|
||||
|
||||
def on_settings_load(self):
|
||||
return {
|
||||
"socket": s.get(["socket"])
|
||||
}
|
||||
|
||||
def on_settings_save(self, data):
|
||||
if "socket" in data and data["socket"]:
|
||||
s.set(["socket"], data["socket"])
|
||||
|
||||
##~~ TemplatePlugin API (part of SettingsPlugin)
|
||||
|
||||
def get_template_vars(self):
|
||||
return dict(
|
||||
_settings_menu_entry="Network connection"
|
||||
)
|
||||
|
||||
def get_template_folder(self):
|
||||
return os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates")
|
||||
|
||||
##~~ SimpleApiPlugin API
|
||||
|
||||
def get_api_commands(self):
|
||||
return {
|
||||
"start_ap": [],
|
||||
"stop_ap": [],
|
||||
"refresh_wifi": [],
|
||||
"configure_wifi": ["ssid", "psk"],
|
||||
}
|
||||
|
||||
def on_api_get(self, request):
|
||||
try:
|
||||
wifis = self._get_wifi_list()
|
||||
status = self._get_status()
|
||||
except Exception as e:
|
||||
return jsonify(dict(error=e.message))
|
||||
|
||||
return jsonify({
|
||||
"wifis": wifis,
|
||||
"status": status
|
||||
})
|
||||
|
||||
def on_api_command(self, command, data):
|
||||
if command == "refresh_wifi":
|
||||
return jsonify(self._get_wifi_list(force=True))
|
||||
|
||||
elif command == "configure_wifi":
|
||||
if data["psk"]:
|
||||
self.logger.info("Configuring wifi {ssid} and psk...".format(**data))
|
||||
else:
|
||||
self.logger.info("Configuring wifi {ssid}...".format(**data))
|
||||
|
||||
elif command == "start_ap":
|
||||
self.logger.info("Starting ap...")
|
||||
|
||||
elif command == "stop_ap":
|
||||
self.logger.info("Stopping ap...")
|
||||
|
||||
##~~ AssetPlugin API
|
||||
|
||||
def get_asset_folder(self):
|
||||
return os.path.join(os.path.dirname(os.path.realpath(__file__)), "static")
|
||||
|
||||
def get_assets(self):
|
||||
return {
|
||||
"js": ["js/netconnectd.js"],
|
||||
"css": ["css/netconnectd.css"],
|
||||
"less": ["less/netconnectd.less"]
|
||||
}
|
||||
|
||||
##~~ Private helpers
|
||||
|
||||
def _get_wifi_list(self, force=False):
|
||||
if force:
|
||||
self.logger.info("Forcing wifi refresh...")
|
||||
return [
|
||||
{"name": "A Test Wifi", "quality": 59, "encrypted": True},
|
||||
{"name": "TyrionDiesOnPage24", "quality": 90, "encrypted": True},
|
||||
{"name": "Giraffenhaus", "quality": 78, "encrypted": False},
|
||||
]
|
||||
|
||||
def _get_status(self):
|
||||
return {
|
||||
"ap": False,
|
||||
"connectedToWifi": True
|
||||
}
|
||||
|
||||
|
||||
__plugin_name__ = "netconnectd client"
|
||||
__plugin_version__ = "0.1"
|
||||
__plugin_description__ = "Client for netconnectd that allows configuration of netconnectd through OctoPrint's settings dialog"
|
||||
__plugin_implementations__ = []
|
||||
|
||||
def __plugin_check__():
|
||||
import sys
|
||||
# TODO arm the check
|
||||
#if not sys.platform == 'linux2':
|
||||
# return False
|
||||
|
||||
global __plugin_implementations__
|
||||
__plugin_implementations__ = [NetconnectdSettingsPlugin()]
|
||||
return True
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
table th.settings_plugin_netconnectd_wifis_name,table td.settings_plugin_netconnectd_wifis_name{text-overflow:ellipsis;text-align:left}table th.settings_plugin_netconnectd_wifis_quality,table td.settings_plugin_netconnectd_wifis_quality{text-align:right;width:70px}table th.settings_plugin_netconnectd_wifis_action,table td.settings_plugin_netconnectd_wifis_action{text-align:center;width:70px}table th.settings_plugin_netconnectd_wifis_action a,table td.settings_plugin_netconnectd_wifis_action a{text-decoration:none;color:#000}table th.settings_plugin_netconnectd_wifis_action a.disabled,table td.settings_plugin_netconnectd_wifis_action a.disabled{color:#ccc;cursor:default}
|
||||
145
src/octoprint/plugins/netconnectd/static/js/netconnectd.js
Normal file
145
src/octoprint/plugins/netconnectd/static/js/netconnectd.js
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
$(function() {
|
||||
function NetconnectdViewModel(parameters) {
|
||||
var self = this;
|
||||
|
||||
self.loginState = parameters[0];
|
||||
self.settingsViewModel = parameters[1];
|
||||
|
||||
self.settings = undefined;
|
||||
|
||||
self.data = {
|
||||
wifis: ko.observableArray([]),
|
||||
status: {
|
||||
ap: ko.observable(),
|
||||
connectedToWifi: ko.observable()
|
||||
}
|
||||
};
|
||||
|
||||
self.editorWifiSsid = ko.observable();
|
||||
self.editorWifiPassphrase1 = ko.observable();
|
||||
self.editorWifiPassphrase2 = ko.observable();
|
||||
|
||||
self.editorWifiPassphraseMismatch = ko.computed(function() {
|
||||
return self.editorWifiPassphrase1() != self.editorWifiPassphrase2();
|
||||
});
|
||||
|
||||
self.connectionStateText = ko.computed(function() {
|
||||
if (self.data.status.ap()) {
|
||||
return gettext("Access Point is active");
|
||||
} else if (self.data.status.connectedToWifi()) {
|
||||
return gettext("Connected to configured Wifi");
|
||||
} else {
|
||||
return gettext("Connected")
|
||||
}
|
||||
});
|
||||
|
||||
// initialize list helper
|
||||
self.listHelper = new ItemListHelper(
|
||||
"wifis",
|
||||
{
|
||||
"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;
|
||||
},
|
||||
"quality": function (a, b) {
|
||||
// sorts descending
|
||||
if (a["quality"] > b["quality"]) return -1;
|
||||
if (a["quality"] < b["quality"]) return 1;
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
{
|
||||
},
|
||||
"quality",
|
||||
[],
|
||||
[],
|
||||
10
|
||||
);
|
||||
|
||||
self.fromResponse = function (response) {
|
||||
if (response.error !== undefined) {
|
||||
return;
|
||||
}
|
||||
ko.mapping.fromJS(response, self.data);
|
||||
self.listHelper.updateItems(response.wifis);
|
||||
};
|
||||
|
||||
self.configureWifi = function(data) {
|
||||
if (data.encrypted) {
|
||||
self.editorWifiSsid(data.ssid);
|
||||
self.editorWifiPassphrase1(undefined);
|
||||
self.editorWifiPassphrase2(undefined);
|
||||
$("#settings_plugin_netconnectd_wificonfig").modal("show");
|
||||
} else {
|
||||
self.confirmWifiConfiguration();
|
||||
}
|
||||
};
|
||||
|
||||
self.confirmWifiConfiguration = function() {
|
||||
self.sendWifiConfig(self.editorWifiSsid(), self.editorWifiPassphrase1(), function() {
|
||||
self.editorWifiSsid(undefined);
|
||||
self.editorWifiPassphrase1(undefined);
|
||||
self.editorWifiPassphrase2(undefined);
|
||||
$("#settings_plugin_netconnectd_wificonfig").modal("hide");
|
||||
})
|
||||
};
|
||||
|
||||
self.sendStartAp = function() {
|
||||
self._postCommand("start_ap", {});
|
||||
};
|
||||
|
||||
self.sendStopAp = function() {
|
||||
self._postCommand("stop_ap", {});
|
||||
};
|
||||
|
||||
self.sendWifiRefresh = function(force) {
|
||||
if (force === undefined) force = false;
|
||||
self._postCommand("list_wifi", {force: force}, function(response) {
|
||||
self.fromResponse({"wifis": response});
|
||||
});
|
||||
};
|
||||
|
||||
self.sendWifiConfig = function(ssid, psk, callback) {
|
||||
self._postCommand("configure_wifi", {ssid: ssid, psk: psk});
|
||||
};
|
||||
|
||||
self._postCommand = function (command, data, successCallback, failureCallback) {
|
||||
var payload = _.extend(data, {command: command});
|
||||
|
||||
$.ajax({
|
||||
url: API_BASEURL + "plugin/netconnectd",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
data: payload,
|
||||
success: function(response) {
|
||||
if (successCallback) successCallback(response);
|
||||
},
|
||||
fail: function() {
|
||||
if (failureCallback) failureCallback();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
self.requestData = function () {
|
||||
$.ajax({
|
||||
url: API_BASEURL + "plugin/netconnectd",
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: self.fromResponse
|
||||
});
|
||||
};
|
||||
|
||||
self.onBeforeBinding = function() {
|
||||
self.settings = self.settingsViewModel.settings;
|
||||
};
|
||||
|
||||
self.onStartup = function() {
|
||||
self.requestData();
|
||||
};
|
||||
}
|
||||
|
||||
// view model class, parameters for constructor, container to bind to
|
||||
ADDITIONAL_VIEWMODELS.push([NetconnectdViewModel, ["loginStateViewModel", "settingsViewModel"], document.getElementById("settings_plugin_netconnectd_dialog")]);
|
||||
});
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
table {
|
||||
th, td {
|
||||
// log files
|
||||
&.settings_plugin_netconnectd_wifis_name {
|
||||
text-overflow: ellipsis;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&.settings_plugin_netconnectd_wifis_quality {
|
||||
text-align: right;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
&.settings_plugin_netconnectd_wifis_action {
|
||||
text-align: center;
|
||||
width: 70px;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #000;
|
||||
|
||||
&.disabled {
|
||||
color: #ccc;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
<div id="settings_plugin_netconnectd_dialog" data-bind="allowBindings: true">
|
||||
<p>
|
||||
<strong>{{ _('Connection state') }}:</strong> <span data-bind="text: connectionStateText"></span>
|
||||
</p>
|
||||
|
||||
<div class="pull-right">
|
||||
<small>
|
||||
{{ _('Sort by') }}: <a href="#" data-bind="click: function() { listHelper.changeSorting('name'); }">{{ _('Name') }} ({{ _('ascending') }})</a> | <a href="#" data-bind="click: function() { listHelper.changeSorting('quality'); }">{{ _('Quality') }} ({{ _('descending') }})</a></a>
|
||||
</small>
|
||||
</div>
|
||||
<table class="table table-striped table-hover table-condensed table-hover" id="log_files">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="settings_plugin_netconnectd_wifis_name">{{ _('Name') }}</th>
|
||||
<th class="settings_plugin_netconnectd_wifis_quality">{{ _('Quality') }}</th>
|
||||
<th class="settings_plugin_netconnectd_wifis_action">{{ _('Action') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-bind="foreach: listHelper.paginatedItems">
|
||||
<tr data-bind="attr: {title: name}">
|
||||
<td class="settings_plugin_netconnectd_wifis_name"><span class="icon-lock" data-bind="invisible: !encrypted"></span> <span data-bind="text: name"></span></td>
|
||||
<td class="settings_plugin_netconnectd_wifis_quality" data-bind="text: quality"></td>
|
||||
<td class="settings_plugin_netconnectd_wifis_action">
|
||||
<button class="button button-small" data-bind="click: function() { if ($root.loginState.isUser()) { $parent.configureWifi($data); } else { return; } }, css: {disabled: !$root.loginState.isUser()}">{{ _('Connect') }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pagination pagination-mini pagination-centered">
|
||||
<ul>
|
||||
<li data-bind="css: {disabled: listHelper.currentPage() === 0}">
|
||||
<a href="#" data-bind="click: listHelper.prevPage">«</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul data-bind="foreach: listHelper.pages">
|
||||
<li data-bind="css: { active: $data.number === $root.listHelper.currentPage(), disabled: $data.number === -1 }">
|
||||
<a href="#" data-bind="text: $data.text, click: function() { $root.listHelper.changePage($data.number); }"></a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li data-bind="css: {disabled: listHelper.currentPage() === listHelper.lastPage()}">
|
||||
<a href="#" data-bind="click: listHelper.nextPage">»</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<small class="muted">{{ _('netconnectd socket') }}: <span data-bind="text: settings.plugins.netconnectd.socket"></span></small>
|
||||
</div>
|
||||
|
||||
<div id="settings_plugin_netconnectd_wificonfig" class="modal hide fade">
|
||||
<div class="modal-header">
|
||||
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">×</a>
|
||||
<h3>{{ _('Configure secured Wifi connection to "%(ssid)s"', ssid='<span data-bind="text: $root.editorWifiSsid"></span>') }}</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings_plugin_netconnectd_wificonfig_passphrase1">{{ _('Password') }}</label>
|
||||
<div class="controls">
|
||||
<input type="password" class="input-block-level" id="settings_plugin_netconnectd_wificonfig_passphrase1" data-bind="value: $root.editorWifiPassphrase1" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" data-bind="css: {error: $root.editorWifiPassphraseMismatch()}">
|
||||
<label class="control-label" for="settings_plugin_netconnectd_wificonfig_passphrase2">{{ _('Repeat Password') }}</label>
|
||||
<div class="controls">
|
||||
<input type="password" class="input-block-level" id="settings_plugin_netconnectd_wificonfig_passphrase2" data-bind="value: $root.editorWifiPassphrase2, valueUpdate: 'afterkeydown'" required>
|
||||
<span class="help-inline" data-bind="visible: $root.editorWifiPassphraseMismatch()">{{ _('Passwords do not match') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" data-dismiss="modal" aria-hidden="true">{{ _('Abort') }}</button>
|
||||
<button class="btn btn-primary" data-bind="click: function() { $root.confirmConfigureWifi(); }, enable: !$root.editorWifiPassphraseMismatch()">{{ _('Confirm') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -7,7 +7,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
|
|||
|
||||
import uuid
|
||||
from sockjs.tornado import SockJSRouter
|
||||
from flask import Flask, render_template, send_from_directory, g, request
|
||||
from flask import Flask, render_template, send_from_directory, g, request, make_response
|
||||
from flask.ext.login import LoginManager
|
||||
from flask.ext.principal import Principal, Permission, RoleNeed, identity_loaded, UserNeed
|
||||
from flask.ext.babel import Babel
|
||||
|
|
@ -30,6 +30,7 @@ gcodeManager = None
|
|||
userManager = None
|
||||
eventManager = None
|
||||
loginManager = None
|
||||
pluginManager = None
|
||||
|
||||
principals = Principal(app)
|
||||
admin_permission = Permission(RoleNeed("admin"))
|
||||
|
|
@ -41,6 +42,7 @@ from octoprint.settings import settings
|
|||
import octoprint.gcodefiles as gcodefiles
|
||||
import octoprint.users as users
|
||||
import octoprint.events as events
|
||||
import octoprint.plugin
|
||||
import octoprint.timelapse
|
||||
import octoprint._version
|
||||
import octoprint.util
|
||||
|
|
@ -88,6 +90,16 @@ def get_locale():
|
|||
|
||||
@app.route("/")
|
||||
def index():
|
||||
settings_plugins = pluginManager.get_implementations(octoprint.plugin.SettingsPlugin)
|
||||
settings_plugin_template_vars = dict()
|
||||
for name, implementation in settings_plugins.items():
|
||||
settings_plugin_template_vars[name] = implementation.get_template_vars()
|
||||
|
||||
asset_plugins = pluginManager.get_implementations(octoprint.plugin.AssetPlugin)
|
||||
asset_plugin_urls = dict()
|
||||
for name, implementation in asset_plugins.items():
|
||||
asset_plugin_urls[name] = implementation.get_assets()
|
||||
|
||||
return render_template(
|
||||
"index.jinja2",
|
||||
webcamStream=settings().get(["webcam", "stream"]),
|
||||
|
|
@ -104,7 +116,9 @@ def index():
|
|||
stylesheet=settings().get(["devel", "stylesheet"]),
|
||||
gcodeMobileThreshold=settings().get(["gcodeViewer", "mobileSizeThreshold"]),
|
||||
gcodeThreshold=settings().get(["gcodeViewer", "sizeThreshold"]),
|
||||
uiApiKey=UI_API_KEY
|
||||
uiApiKey=UI_API_KEY,
|
||||
settingsPlugins=settings_plugin_template_vars,
|
||||
assetPlugins=asset_plugin_urls
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -113,6 +127,20 @@ def robotsTxt():
|
|||
return send_from_directory(app.static_folder, "robots.txt")
|
||||
|
||||
|
||||
@app.route("/plugin_assets/<string:name>/<path:filename>")
|
||||
def plugin_assets(name, filename):
|
||||
asset_plugins = pluginManager.get_implementations(octoprint.plugin.AssetPlugin)
|
||||
|
||||
if not name in asset_plugins:
|
||||
return make_response(404)
|
||||
asset_plugin = asset_plugins[name]
|
||||
asset_folder = asset_plugin.get_asset_folder()
|
||||
if asset_folder is None:
|
||||
make_response(404)
|
||||
|
||||
return send_from_directory(asset_folder, filename)
|
||||
|
||||
|
||||
@identity_loaded.connect_via(app)
|
||||
def on_identity_loaded(sender, identity):
|
||||
user = load_user(identity.id)
|
||||
|
|
@ -155,6 +183,7 @@ class Server():
|
|||
global userManager
|
||||
global eventManager
|
||||
global loginManager
|
||||
global pluginManager
|
||||
global debug
|
||||
|
||||
from tornado.ioloop import IOLoop
|
||||
|
|
@ -174,6 +203,23 @@ class Server():
|
|||
eventManager = events.eventManager()
|
||||
gcodeManager = gcodefiles.GcodeManager()
|
||||
printer = Printer(gcodeManager)
|
||||
pluginManager = octoprint.plugin.plugin_manager(init=True)
|
||||
|
||||
# configure additional template folders for jinja2
|
||||
template_plugins = pluginManager.get_implementations(octoprint.plugin.TemplatePlugin)
|
||||
additional_template_folders = []
|
||||
for plugin in template_plugins.values():
|
||||
folder = plugin.get_template_folder()
|
||||
if folder is not None:
|
||||
additional_template_folders.append(plugin.get_template_folder())
|
||||
|
||||
import jinja2
|
||||
jinja_loader = jinja2.ChoiceLoader([
|
||||
app.jinja_loader,
|
||||
jinja2.FileSystemLoader(additional_template_folders)
|
||||
])
|
||||
app.jinja_loader = jinja_loader
|
||||
del jinja2
|
||||
|
||||
# configure timelapse
|
||||
octoprint.timelapse.configureTimelapse()
|
||||
|
|
@ -213,7 +259,6 @@ class Server():
|
|||
if self._port is None:
|
||||
self._port = settings().getInt(["server", "port"])
|
||||
|
||||
logger.info("Listening on http://%s:%d" % (self._host, self._port))
|
||||
app.debug = self._debug
|
||||
|
||||
from octoprint.server.api import api
|
||||
|
|
@ -249,6 +294,12 @@ class Server():
|
|||
observer.schedule(util.watchdog.UploadCleanupWatchdogHandler(gcodeManager), settings().getBaseFolder("uploads"))
|
||||
observer.start()
|
||||
|
||||
# now it's the turn of the startup plugins
|
||||
startup_plugins = pluginManager.get_implementations(octoprint.plugin.StartupPlugin)
|
||||
for name, plugin in startup_plugins.items():
|
||||
plugin.on_startup(self._host, self._port)
|
||||
|
||||
logger.info("Listening on http://%s:%d" % (self._host, self._port))
|
||||
try:
|
||||
IOLoop.instance().start()
|
||||
except KeyboardInterrupt:
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from flask.ext.principal import Identity, identity_changed, AnonymousIdentity
|
|||
import octoprint.util as util
|
||||
import octoprint.users
|
||||
import octoprint.server
|
||||
import octoprint.plugin
|
||||
from octoprint.server import admin_permission, NO_CONTENT, UI_API_KEY
|
||||
from octoprint.settings import settings as s, valid_boolean_trues
|
||||
from octoprint.server.util import get_api_key, get_user_for_apikey
|
||||
|
|
@ -107,10 +108,49 @@ def afterApiRequests(resp):
|
|||
return resp
|
||||
|
||||
|
||||
#~~ data from plugins
|
||||
|
||||
@api.route("/plugin/<string:name>", methods=["GET"])
|
||||
def pluginData(name):
|
||||
api_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SimpleApiPlugin)
|
||||
if not name in api_plugins:
|
||||
return make_response(404)
|
||||
|
||||
api_plugin = api_plugins[name]
|
||||
response = api_plugin.on_api_get(request)
|
||||
|
||||
if response is not None:
|
||||
return response
|
||||
return NO_CONTENT
|
||||
|
||||
#~~ commands for plugins
|
||||
|
||||
@api.route("/plugin/<string:name>", methods=["POST"])
|
||||
@restricted_access
|
||||
def pluginCommand(name):
|
||||
api_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SimpleApiPlugin)
|
||||
if not name in api_plugins:
|
||||
return make_response(404)
|
||||
|
||||
api_plugin = api_plugins[name]
|
||||
valid_commands = api_plugin.get_api_commands()
|
||||
if valid_commands is None:
|
||||
return make_response(405)
|
||||
|
||||
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
|
||||
if response is not None:
|
||||
return response
|
||||
|
||||
response = api_plugin.on_api_command(command, data)
|
||||
if response is not None:
|
||||
return response
|
||||
return NO_CONTENT
|
||||
|
||||
#~~ first run setup
|
||||
|
||||
|
||||
@api.route("/setup", methods=["POST"])
|
||||
@restricted_access
|
||||
def firstRunSetup():
|
||||
if not s().getBoolean(["server", "firstRun"]):
|
||||
abort(403)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ from octoprint.server import admin_permission
|
|||
from octoprint.server.api import api
|
||||
from octoprint.server.util.flask import restricted_access
|
||||
|
||||
import octoprint.plugin
|
||||
|
||||
|
||||
#~~ settings
|
||||
|
||||
|
|
@ -29,7 +31,7 @@ def getSettings():
|
|||
|
||||
connectionOptions = getConnectionOptions()
|
||||
|
||||
return jsonify({
|
||||
data = {
|
||||
"api": {
|
||||
"enabled": s.getBoolean(["api", "enabled"]),
|
||||
"key": s.get(["api", "key"]),
|
||||
|
|
@ -102,7 +104,19 @@ def getSettings():
|
|||
"path": s.get(["cura", "path"]),
|
||||
"config": s.get(["cura", "config"])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
settings_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SettingsPlugin)
|
||||
for name, plugin in settings_plugins.items():
|
||||
plugin_data = plugin.on_settings_load()
|
||||
if plugin_data:
|
||||
if not "plugins" in data:
|
||||
data["plugins"] = dict()
|
||||
if "__enabled" in plugin_data:
|
||||
del plugin_data["__enabled"]
|
||||
data["plugins"][name] = plugin_data
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@api.route("/settings", methods=["POST"])
|
||||
|
|
@ -204,6 +218,11 @@ def setSettings():
|
|||
enabled = cura.get("enabled")
|
||||
s.setBoolean(["cura", "enabled"], enabled)
|
||||
|
||||
settings_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SettingsPlugin)
|
||||
for name, plugin in settings_plugins.items():
|
||||
if "plugins" in data and name in data["plugins"]:
|
||||
plugin.on_settings_save(data["plugins"][name])
|
||||
|
||||
s.save()
|
||||
|
||||
return getSettings()
|
||||
|
|
|
|||
|
|
@ -91,7 +91,8 @@ default_settings = {
|
|||
"timelapse_tmp": None,
|
||||
"logs": None,
|
||||
"virtualSd": None,
|
||||
"watched": None
|
||||
"watched": None,
|
||||
"plugins": None
|
||||
},
|
||||
"temperature": {
|
||||
"profiles":
|
||||
|
|
@ -152,6 +153,7 @@ default_settings = {
|
|||
{ "name": "Suppress M105 requests/responses", "regex": "(Send: M105)|(Recv: ok T\d*:)" },
|
||||
{ "name": "Suppress M27 requests/responses", "regex": "(Send: M27)|(Recv: SD printing byte)" }
|
||||
],
|
||||
"plugins": {},
|
||||
"devel": {
|
||||
"stylesheet": "css",
|
||||
"virtualPrinter": {
|
||||
|
|
@ -301,19 +303,6 @@ class Settings(object):
|
|||
self.save(force=True)
|
||||
self._logger.info("Migrated %d event subscriptions to new format and structure" % len(newEvents["subscriptions"]))
|
||||
|
||||
if migrate:
|
||||
self._migrateConfig()
|
||||
|
||||
def _migrateConfig(self):
|
||||
if not self._config:
|
||||
return
|
||||
|
||||
dirty = False
|
||||
for migrate in (self._migrate_event_config, self._migrate_reverse_proxy_config):
|
||||
dirty = migrate() or dirty
|
||||
if dirty:
|
||||
self.save(force=True)
|
||||
|
||||
def _migrate_reverse_proxy_config(self):
|
||||
if "server" in self._config.keys() and ("baseUrl" in self._config["server"] or "scheme" in self._config["server"]):
|
||||
prefix = ""
|
||||
|
|
@ -435,12 +424,13 @@ class Settings(object):
|
|||
|
||||
#~~ getter
|
||||
|
||||
def get(self, path, asdict=False):
|
||||
def get(self, path, asdict=False, defaults=None):
|
||||
if len(path) == 0:
|
||||
return None
|
||||
|
||||
config = self._config
|
||||
defaults = default_settings
|
||||
if defaults is None:
|
||||
defaults = default_settings
|
||||
|
||||
while len(path) > 1:
|
||||
key = path.pop(0)
|
||||
|
|
@ -484,8 +474,8 @@ class Settings(object):
|
|||
else:
|
||||
return results
|
||||
|
||||
def getInt(self, path):
|
||||
value = self.get(path)
|
||||
def getInt(self, path, defaults=None):
|
||||
value = self.get(path, defaults=defaults)
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
|
|
@ -495,8 +485,8 @@ class Settings(object):
|
|||
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)
|
||||
def getFloat(self, path, defaults=None):
|
||||
value = self.get(path, defaults=defaults)
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
|
|
@ -506,8 +496,8 @@ class Settings(object):
|
|||
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)
|
||||
def getBoolean(self, path, defaults=None):
|
||||
value = self.get(path, defaults=defaults)
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, bool):
|
||||
|
|
@ -581,12 +571,13 @@ class Settings(object):
|
|||
|
||||
#~~ setter
|
||||
|
||||
def set(self, path, value, force=False):
|
||||
def set(self, path, value, force=False, defaults=None):
|
||||
if len(path) == 0:
|
||||
return
|
||||
|
||||
config = self._config
|
||||
defaults = default_settings
|
||||
if defaults is None:
|
||||
defaults = default_settings
|
||||
|
||||
while len(path) > 1:
|
||||
key = path.pop(0)
|
||||
|
|
@ -611,9 +602,9 @@ class Settings(object):
|
|||
config[key] = value
|
||||
self._dirty = True
|
||||
|
||||
def setInt(self, path, value, force=False):
|
||||
def setInt(self, path, value, force=False, defaults=None):
|
||||
if value is None:
|
||||
self.set(path, None, force)
|
||||
self.set(path, None, force=force, defaults=defaults)
|
||||
return
|
||||
|
||||
try:
|
||||
|
|
@ -624,9 +615,9 @@ class Settings(object):
|
|||
|
||||
self.set(path, intValue, force)
|
||||
|
||||
def setFloat(self, path, value, force=False):
|
||||
def setFloat(self, path, value, force=False, defaults=None):
|
||||
if value is None:
|
||||
self.set(path, None, force)
|
||||
self.set(path, None, force=force, defaults=defaults)
|
||||
return
|
||||
|
||||
try:
|
||||
|
|
@ -637,13 +628,13 @@ class Settings(object):
|
|||
|
||||
self.set(path, floatValue, force)
|
||||
|
||||
def setBoolean(self, path, value, force=False):
|
||||
def setBoolean(self, path, value, force=False, defaults=None):
|
||||
if value is None or isinstance(value, bool):
|
||||
self.set(path, value, force)
|
||||
self.set(path, value, force=force, defaults=defaults)
|
||||
elif value.lower() in valid_boolean_trues:
|
||||
self.set(path, True, force)
|
||||
self.set(path, True, force=force, defaults=defaults)
|
||||
else:
|
||||
self.set(path, False, force)
|
||||
self.set(path, False, force=force, defaults=defaults)
|
||||
|
||||
def setBaseFolder(self, type, path, force=False):
|
||||
if type not in default_settings["folder"].keys():
|
||||
|
|
|
|||
|
|
@ -1,16 +1,7 @@
|
|||
function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewModel, temperatureViewModel, controlViewModel, terminalViewModel, gcodeFilesViewModel, timelapseViewModel, gcodeViewModel, logViewModel) {
|
||||
function DataUpdater(allViewModels) {
|
||||
var self = this;
|
||||
|
||||
self.loginStateViewModel = loginStateViewModel;
|
||||
self.connectionViewModel = connectionViewModel;
|
||||
self.printerStateViewModel = printerStateViewModel;
|
||||
self.temperatureViewModel = temperatureViewModel;
|
||||
self.controlViewModel = controlViewModel;
|
||||
self.terminalViewModel = terminalViewModel;
|
||||
self.gcodeFilesViewModel = gcodeFilesViewModel;
|
||||
self.timelapseViewModel = timelapseViewModel;
|
||||
self.gcodeViewModel = gcodeViewModel;
|
||||
self.logViewModel = logViewModel;
|
||||
self.allViewModels = allViewModels;
|
||||
|
||||
self._socket = undefined;
|
||||
self._autoReconnecting = false;
|
||||
|
|
@ -60,6 +51,10 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM
|
|||
|
||||
self._onmessage = function(e) {
|
||||
for (var prop in e.data) {
|
||||
if (!e.data.hasOwnProperty(prop)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var data = e.data[prop];
|
||||
|
||||
switch (prop) {
|
||||
|
|
@ -76,12 +71,11 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM
|
|||
|
||||
if ($("#offline_overlay").is(":visible")) {
|
||||
$("#offline_overlay").hide();
|
||||
self.logViewModel.requestData();
|
||||
self.timelapseViewModel.requestData();
|
||||
$("#webcam_image").attr("src", CONFIG_WEBCAM_STREAM + "?" + new Date().getTime());
|
||||
self.loginStateViewModel.requestData();
|
||||
self.gcodeFilesViewModel.requestData();
|
||||
self.gcodeViewModel.reset();
|
||||
_.each(self.allViewModels, function(viewModel) {
|
||||
if (viewModel.hasOwnProperty("onDataUpdaterReconnect")) {
|
||||
viewModel.onDataUpdaterReconnect();
|
||||
}
|
||||
});
|
||||
|
||||
if ($('#tabs li[class="active"] a').attr("href") == "#control") {
|
||||
$("#webcam_image").attr("src", CONFIG_WEBCAM_STREAM + "?" + new Date().getTime());
|
||||
|
|
@ -91,25 +85,19 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM
|
|||
break;
|
||||
}
|
||||
case "history": {
|
||||
self.connectionViewModel.fromHistoryData(data);
|
||||
self.printerStateViewModel.fromHistoryData(data);
|
||||
self.temperatureViewModel.fromHistoryData(data);
|
||||
self.controlViewModel.fromHistoryData(data);
|
||||
self.terminalViewModel.fromHistoryData(data);
|
||||
self.timelapseViewModel.fromHistoryData(data);
|
||||
self.gcodeViewModel.fromHistoryData(data);
|
||||
self.gcodeFilesViewModel.fromCurrentData(data);
|
||||
_.each(self.allViewModels, function(viewModel) {
|
||||
if (viewModel.hasOwnProperty("fromHistoryData")) {
|
||||
viewModel.fromHistoryData(data);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "current": {
|
||||
self.connectionViewModel.fromCurrentData(data);
|
||||
self.printerStateViewModel.fromCurrentData(data);
|
||||
self.temperatureViewModel.fromCurrentData(data);
|
||||
self.controlViewModel.fromCurrentData(data);
|
||||
self.terminalViewModel.fromCurrentData(data);
|
||||
self.timelapseViewModel.fromCurrentData(data);
|
||||
self.gcodeViewModel.fromCurrentData(data);
|
||||
self.gcodeFilesViewModel.fromCurrentData(data);
|
||||
_.each(self.allViewModels, function(viewModel) {
|
||||
if (viewModel.hasOwnProperty("fromCurrentData")) {
|
||||
viewModel.fromCurrentData(data);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "event": {
|
||||
|
|
@ -162,11 +150,19 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM
|
|||
break;
|
||||
}
|
||||
case "feedbackCommandOutput": {
|
||||
self.controlViewModel.fromFeedbackCommandData(data);
|
||||
_.each(self.allViewModels, function(viewModel) {
|
||||
if (viewModel.hasOwnProperty("fromFeedbackCommandData")) {
|
||||
viewModel.fromFeedbackCommandData(data);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "timelapse": {
|
||||
self.printerStateViewModel.fromTimelapseData(data);
|
||||
_.each(self.allViewModels, function(viewModel) {
|
||||
if (viewModel.hasOwnProperty("fromTimelapseData")) {
|
||||
viewModel.fromTimelapseData(data);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,35 +25,6 @@ $(function() {
|
|||
gettext("Transfering file to SD")
|
||||
];
|
||||
|
||||
//~~ Initialize view models
|
||||
var loginStateViewModel = new LoginStateViewModel();
|
||||
var usersViewModel = new UsersViewModel(loginStateViewModel);
|
||||
var settingsViewModel = new SettingsViewModel(loginStateViewModel, usersViewModel);
|
||||
var connectionViewModel = new ConnectionViewModel(loginStateViewModel, settingsViewModel);
|
||||
var timelapseViewModel = new TimelapseViewModel(loginStateViewModel);
|
||||
var printerStateViewModel = new PrinterStateViewModel(loginStateViewModel, timelapseViewModel);
|
||||
var appearanceViewModel = new AppearanceViewModel(settingsViewModel);
|
||||
var temperatureViewModel = new TemperatureViewModel(loginStateViewModel, settingsViewModel);
|
||||
var controlViewModel = new ControlViewModel(loginStateViewModel, settingsViewModel);
|
||||
var terminalViewModel = new TerminalViewModel(loginStateViewModel, settingsViewModel);
|
||||
var gcodeFilesViewModel = new GcodeFilesViewModel(printerStateViewModel, loginStateViewModel);
|
||||
var gcodeViewModel = new GcodeViewModel(loginStateViewModel, settingsViewModel);
|
||||
var navigationViewModel = new NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel, usersViewModel);
|
||||
var logViewModel = new LogViewModel(loginStateViewModel);
|
||||
|
||||
var dataUpdater = new DataUpdater(
|
||||
loginStateViewModel,
|
||||
connectionViewModel,
|
||||
printerStateViewModel,
|
||||
temperatureViewModel,
|
||||
controlViewModel,
|
||||
terminalViewModel,
|
||||
gcodeFilesViewModel,
|
||||
timelapseViewModel,
|
||||
gcodeViewModel,
|
||||
logViewModel
|
||||
);
|
||||
|
||||
PNotify.prototype.options.styling = "bootstrap2";
|
||||
|
||||
// work around a stupid iOS6 bug where ajax requests get cached and only work once, as described at
|
||||
|
|
@ -71,13 +42,70 @@ $(function() {
|
|||
//~~ Show settings - to ensure centered
|
||||
$('#navbar_show_settings').click(function() {
|
||||
$('#settings_dialog').modal()
|
||||
.css({
|
||||
width: 'auto',
|
||||
'margin-left': function() { return -($(this).width() /2); }
|
||||
});
|
||||
.css({
|
||||
width: 'auto',
|
||||
'margin-left': function() { return -($(this).width() /2); }
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
//~~ Initialize view models
|
||||
var loginStateViewModel = new LoginStateViewModel();
|
||||
var usersViewModel = new UsersViewModel(loginStateViewModel);
|
||||
var settingsViewModel = new SettingsViewModel(loginStateViewModel, usersViewModel);
|
||||
var connectionViewModel = new ConnectionViewModel(loginStateViewModel, settingsViewModel);
|
||||
var timelapseViewModel = new TimelapseViewModel(loginStateViewModel);
|
||||
var printerStateViewModel = new PrinterStateViewModel(loginStateViewModel, timelapseViewModel);
|
||||
var appearanceViewModel = new AppearanceViewModel(settingsViewModel);
|
||||
var temperatureViewModel = new TemperatureViewModel(loginStateViewModel, settingsViewModel);
|
||||
var controlViewModel = new ControlViewModel(loginStateViewModel, settingsViewModel);
|
||||
var terminalViewModel = new TerminalViewModel(loginStateViewModel, settingsViewModel);
|
||||
var gcodeFilesViewModel = new GcodeFilesViewModel(printerStateViewModel, loginStateViewModel);
|
||||
var gcodeViewModel = new GcodeViewModel(loginStateViewModel, settingsViewModel);
|
||||
var navigationViewModel = new NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel, usersViewModel);
|
||||
var logViewModel = new LogViewModel(loginStateViewModel);
|
||||
|
||||
var viewModelMap = {
|
||||
loginStateViewModel: loginStateViewModel,
|
||||
usersViewModel: usersViewModel,
|
||||
settingsViewModel: settingsViewModel,
|
||||
connectionViewModel: connectionViewModel,
|
||||
timelapseViewModel: timelapseViewModel,
|
||||
printerStateViewModel: printerStateViewModel,
|
||||
appearanceViewModel: appearanceViewModel,
|
||||
temperatureViewModel: temperatureViewModel,
|
||||
controlViewModel: controlViewModel,
|
||||
terminalViewModel: terminalViewModel,
|
||||
gcodeFilesViewModel: gcodeFilesViewModel,
|
||||
gcodeViewModel: gcodeViewModel,
|
||||
navigationViewModel: navigationViewModel,
|
||||
logViewModel: logViewModel
|
||||
};
|
||||
|
||||
var allViewModels = _.values(viewModelMap);
|
||||
|
||||
var additionalViewModels = [];
|
||||
_.each(ADDITIONAL_VIEWMODELS, function(viewModel) {
|
||||
var viewModelClass = viewModel[0];
|
||||
var viewModelParameters = viewModel[1];
|
||||
var viewModelBindTarget = viewModel[2];
|
||||
|
||||
var constructorParameters = [];
|
||||
_.each(viewModelParameters, function(parameter) {
|
||||
if (_.has(viewModelMap, parameter)) {
|
||||
constructorParameters.push(viewModelMap[parameter]);
|
||||
} else {
|
||||
constructorParameters.push(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
var viewModelInstance = new viewModelClass(constructorParameters);
|
||||
additionalViewModels.push([viewModelInstance, viewModelBindTarget]);
|
||||
allViewModels.push(viewModelInstance);
|
||||
});
|
||||
|
||||
var dataUpdater = new DataUpdater(allViewModels);
|
||||
|
||||
//~~ Temperature
|
||||
|
||||
$('#tabs a[data-toggle="tab"]').on('shown', function (e) {
|
||||
|
|
@ -331,37 +359,62 @@ $(function() {
|
|||
}
|
||||
};
|
||||
|
||||
ko.applyBindings(connectionViewModel, document.getElementById("connection_accordion"));
|
||||
ko.applyBindings(printerStateViewModel, document.getElementById("state_accordion"));
|
||||
ko.applyBindings(gcodeFilesViewModel, document.getElementById("files_accordion"));
|
||||
ko.applyBindings(temperatureViewModel, document.getElementById("temp"));
|
||||
ko.applyBindings(controlViewModel, document.getElementById("control"));
|
||||
ko.applyBindings(terminalViewModel, document.getElementById("term"));
|
||||
var gcode = document.getElementById("gcode");
|
||||
if (gcode) {
|
||||
gcodeViewModel.initialize();
|
||||
ko.applyBindings(gcodeViewModel, gcode);
|
||||
}
|
||||
ko.applyBindings(settingsViewModel, document.getElementById("settings_dialog"));
|
||||
ko.applyBindings(navigationViewModel, document.getElementById("navbar"));
|
||||
ko.applyBindings(appearanceViewModel, document.getElementsByTagName("head")[0]);
|
||||
ko.applyBindings(printerStateViewModel, document.getElementById("drop_overlay"));
|
||||
ko.applyBindings(logViewModel, document.getElementById("logs"));
|
||||
ko.bindingHandlers.invisible = {
|
||||
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
|
||||
if (!valueAccessor()) return;
|
||||
ko.bindingHandlers.style.update(element, function() {
|
||||
return { visibility: 'hidden' };
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
var timelapseElement = document.getElementById("timelapse");
|
||||
if (timelapseElement) {
|
||||
ko.applyBindings(timelapseViewModel, timelapseElement);
|
||||
}
|
||||
settingsViewModel.requestData(function() {
|
||||
ko.applyBindings(settingsViewModel, document.getElementById("settings_dialog"));
|
||||
|
||||
ko.applyBindings(connectionViewModel, document.getElementById("connection_accordion"));
|
||||
ko.applyBindings(printerStateViewModel, document.getElementById("state_accordion"));
|
||||
ko.applyBindings(gcodeFilesViewModel, document.getElementById("files_accordion"));
|
||||
ko.applyBindings(temperatureViewModel, document.getElementById("temp"));
|
||||
ko.applyBindings(controlViewModel, document.getElementById("control"));
|
||||
ko.applyBindings(terminalViewModel, document.getElementById("term"));
|
||||
var gcode = document.getElementById("gcode");
|
||||
if (gcode) {
|
||||
gcodeViewModel.initialize();
|
||||
ko.applyBindings(gcodeViewModel, gcode);
|
||||
}
|
||||
//ko.applyBindings(settingsViewModel, document.getElementById("settings_dialog"));
|
||||
ko.applyBindings(navigationViewModel, document.getElementById("navbar"));
|
||||
ko.applyBindings(appearanceViewModel, document.getElementsByTagName("head")[0]);
|
||||
ko.applyBindings(printerStateViewModel, document.getElementById("drop_overlay"));
|
||||
ko.applyBindings(logViewModel, document.getElementById("logs"));
|
||||
|
||||
var timelapseElement = document.getElementById("timelapse");
|
||||
if (timelapseElement) {
|
||||
ko.applyBindings(timelapseViewModel, timelapseElement);
|
||||
}
|
||||
|
||||
// apply bindings and signal startup
|
||||
_.each(additionalViewModels, function(additionalViewModel) {
|
||||
if (additionalViewModel[0].hasOwnProperty("onBeforeBinding")) {
|
||||
additionalViewModel[0].onBeforeBinding();
|
||||
}
|
||||
|
||||
// model instance, target container
|
||||
ko.applyBindings(additionalViewModel[0], additionalViewModel[1]);
|
||||
|
||||
if (additionalViewModel[0].hasOwnProperty("onAfterBinding")) {
|
||||
additionalViewModel[0].onAfterBinding();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//~~ startup commands
|
||||
|
||||
loginStateViewModel.requestData();
|
||||
connectionViewModel.requestData();
|
||||
controlViewModel.requestData();
|
||||
gcodeFilesViewModel.requestData();
|
||||
timelapseViewModel.requestData();
|
||||
settingsViewModel.requestData();
|
||||
logViewModel.requestData();
|
||||
_.each(allViewModels, function(viewModel) {
|
||||
if (viewModel.hasOwnProperty("onStartup")) {
|
||||
viewModel.onStartup();
|
||||
}
|
||||
});
|
||||
|
||||
loginStateViewModel.subscribe(function(change, data) {
|
||||
if ("login" == change) {
|
||||
|
|
|
|||
|
|
@ -120,4 +120,8 @@ function ConnectionViewModel(loginStateViewModel, settingsViewModel) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
self.onStartup = function() {
|
||||
self.requestData();
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -236,6 +236,9 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
|
|||
default:
|
||||
return "customControls_emptyTemplate";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.onStartup = function() {
|
||||
self.requestData();
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -304,5 +304,12 @@ function GcodeFilesViewModel(printerStateViewModel, loginStateViewModel) {
|
|||
}
|
||||
};
|
||||
|
||||
self.onDataUpdaterReconnect = function() {
|
||||
self.requestData();
|
||||
};
|
||||
|
||||
self.onStartup = function() {
|
||||
self.requestData();
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -405,4 +405,8 @@ function GcodeViewModel(loginStateViewModel, settingsViewModel) {
|
|||
GCODE.ui.changeSelectedCommands(self.layerSlider.slider("getValue"), tuple[0], tuple[1]);
|
||||
};
|
||||
|
||||
self.onDataUpdaterReconnect = function() {
|
||||
self.reset();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ function LogViewModel(loginStateViewModel) {
|
|||
return;
|
||||
|
||||
self.listHelper.updateItems(files);
|
||||
}
|
||||
};
|
||||
|
||||
self.removeFile = function(filename) {
|
||||
$.ajax({
|
||||
|
|
@ -58,5 +58,13 @@ function LogViewModel(loginStateViewModel) {
|
|||
dataType: "json",
|
||||
success: self.requestData
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
self.onDataUpdaterReconnect = function() {
|
||||
self.requestData();
|
||||
};
|
||||
|
||||
self.onStartup = function() {
|
||||
self.requestData();
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ function LoginStateViewModel() {
|
|||
data: {"passive": true},
|
||||
success: self.fromResponse
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
self.fromResponse = function(response) {
|
||||
if (response && response.name) {
|
||||
|
|
@ -85,5 +85,13 @@ function LoginStateViewModel() {
|
|||
self.fromResponse(response);
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
self.onDataUpdaterReconnect = function() {
|
||||
self.requestData();
|
||||
};
|
||||
|
||||
self.onStartup = function() {
|
||||
self.requestData();
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
|
|||
{key: "green", name: gettext("green")},
|
||||
{key: "blue", name: gettext("blue")},
|
||||
{key: "violet", name: gettext("violet")},
|
||||
{key: "black", name: gettext("black")},
|
||||
{key: "black", name: gettext("black")}
|
||||
]);
|
||||
|
||||
self.appearance_colorName = function(color) {
|
||||
switch (color) {
|
||||
case "red":
|
||||
|
|
@ -166,6 +167,8 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
|
|||
|
||||
self.terminalFilters = ko.observableArray([]);
|
||||
|
||||
self.settings = undefined;
|
||||
|
||||
self.addTemperatureProfile = function() {
|
||||
self.temperature_profiles.push({name: "New", extruder:0, bed:0});
|
||||
};
|
||||
|
|
@ -220,6 +223,12 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
|
|||
};
|
||||
|
||||
self.fromResponse = function(response) {
|
||||
if (self.settings === undefined) {
|
||||
self.settings = ko.mapping.fromJS(response);
|
||||
} else {
|
||||
ko.mapping.fromJS(response, self.settings);
|
||||
}
|
||||
|
||||
self.api_enabled(response.api.enabled);
|
||||
self.api_key(response.api.key);
|
||||
self.api_allowCrossOrigin(response.api.allowCrossOrigin);
|
||||
|
|
@ -284,7 +293,9 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
|
|||
};
|
||||
|
||||
self.saveData = function() {
|
||||
var data = {
|
||||
var data = ko.mapping.toJS(self.settings);
|
||||
|
||||
data = _.extend(data, {
|
||||
"api" : {
|
||||
"enabled": self.api_enabled(),
|
||||
"key": self.api_key(),
|
||||
|
|
@ -354,7 +365,7 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
|
|||
"config": self.cura_config()
|
||||
},
|
||||
"terminalFilters": self.terminalFilters()
|
||||
};
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
url: API_BASEURL + "settings",
|
||||
|
|
@ -367,6 +378,6 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
|
|||
$("#settings_dialog").modal("hide");
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,15 +94,15 @@ function TimelapseViewModel(loginStateViewModel) {
|
|||
|
||||
self.persist(false);
|
||||
self.isDirty(false);
|
||||
}
|
||||
};
|
||||
|
||||
self.fromCurrentData = function(data) {
|
||||
self._processStateData(data.state);
|
||||
}
|
||||
};
|
||||
|
||||
self.fromHistoryData = function(data) {
|
||||
self._processStateData(data.state);
|
||||
}
|
||||
};
|
||||
|
||||
self._processStateData = function(data) {
|
||||
self.isErrorOrClosed(data.flags.closedOrError);
|
||||
|
|
@ -112,7 +112,7 @@ function TimelapseViewModel(loginStateViewModel) {
|
|||
self.isError(data.flags.error);
|
||||
self.isReady(data.flags.ready);
|
||||
self.isLoading(data.flags.loading);
|
||||
}
|
||||
};
|
||||
|
||||
self.removeFile = function(filename) {
|
||||
$.ajax({
|
||||
|
|
@ -121,25 +121,33 @@ function TimelapseViewModel(loginStateViewModel) {
|
|||
dataType: "json",
|
||||
success: self.requestData
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
self.save = function(data, event) {
|
||||
var data = {
|
||||
var payload = {
|
||||
"type": self.timelapseType(),
|
||||
"postRoll": self.timelapsePostRoll(),
|
||||
"save": self.persist()
|
||||
}
|
||||
};
|
||||
|
||||
if (self.timelapseType() == "timed") {
|
||||
data["interval"] = self.timelapseTimedInterval();
|
||||
payload["interval"] = self.timelapseTimedInterval();
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: API_BASEURL + "timelapse",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
data: data,
|
||||
data: payload,
|
||||
success: self.fromResponse
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
self.onDataUpdaterReconnect = function() {
|
||||
self.requestData();
|
||||
};
|
||||
|
||||
self.onStartup = function() {
|
||||
self.requestData();
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,11 +13,32 @@
|
|||
<link href="{{ url_for('static', filename='css/font-awesome.min.css') }}" rel="stylesheet" media="screen">
|
||||
<link href="{{ url_for('static', filename='css/jquery.fileupload-ui.css') }}" rel="stylesheet" media="screen">
|
||||
<link href="{{ url_for('static', filename='css/pnotify.min.css') }}" rel="stylesheet" media="screen">
|
||||
|
||||
{% if stylesheet == "less" %}
|
||||
<link href="{{ url_for('static', filename='less/octoprint.less') }}" rel="stylesheet/less" type="text/css" media="screen">
|
||||
|
||||
<!-- Plugin files -->
|
||||
{% for name, assets in assetPlugins.items() %}
|
||||
{% if "less" in assets %}
|
||||
{% for asset in assets["less"] %}
|
||||
<link href="{{ url_for('plugin_assets', name=name, filename=asset) }}" rel="stylesheet/less" type="text/css" media="screen">
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<!-- /Plugin files -->
|
||||
|
||||
<script src="{{ url_for('static', filename='js/lib/less.min.js') }}" type="text/javascript"></script>
|
||||
{% else %}
|
||||
<link href="{{ url_for('static', filename='css/octoprint.css') }}" rel="stylesheet" type="text/css" media="screen">
|
||||
<!-- Plugin files -->
|
||||
{% for name, assets in assetPlugins.items() %}
|
||||
{% if "css" in assets %}
|
||||
{% for asset in assets["css"] %}
|
||||
<link href="{{ url_for('plugin_assets', name=name, filename=asset) }}" rel="stylesheet" type="text/css" media="screen">
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<!-- /Plugin files -->
|
||||
{% endif %}
|
||||
|
||||
<script lang="javascript">
|
||||
|
|
@ -44,6 +65,8 @@
|
|||
var VERSION = "{{ version }}";
|
||||
var DISPLAY_VERSION = "{{ display_version }}";
|
||||
var LOCALE = "{{ g.locale }}";
|
||||
|
||||
var ADDITIONAL_VIEWMODELS = [];
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -609,6 +632,7 @@
|
|||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/underscore-min.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/underscore.string.min.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/knockout.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/knockout.mapping-latest.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/babel.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/avltree.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/bootstrap/bootstrap.js') }}"></script>
|
||||
|
|
@ -647,6 +671,16 @@
|
|||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/users.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/log.js') }}"></script>
|
||||
|
||||
<!-- Plugin files -->
|
||||
{% for name, assets in assetPlugins.items() %}
|
||||
{% if "js" in assets %}
|
||||
{% for asset in assets["js"] %}
|
||||
<script type="text/javascript" src="{{ url_for('plugin_assets', name=name, filename=asset) }}"></script>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<!-- /Plugin files -->
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/dataupdater.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/helpers.js') }}"></script>
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,14 @@
|
|||
<li><a href="#settings_folder" data-toggle="tab">{{ _('Folders') }}</a></li>
|
||||
<li><a href="#settings_appearance" data-toggle="tab">{{ _('Appearance') }}</a></li>
|
||||
<li><a href="#settings_logs" data-toggle="tab">{{ _('Logs') }}</a></li>
|
||||
{% if settingsPlugins %}
|
||||
<li class="nav-header">Plugins</li>
|
||||
{% for plugin_name, vars in settingsPlugins.items() %}
|
||||
{% if vars._settings_menu_entry %}
|
||||
<li><a href="#settings_plugin_{{ plugin_name }}" data-toggle="tab">{{ vars._settings_menu_entry }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<div class="tab-content span8">
|
||||
|
|
@ -621,6 +629,12 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for plugin_name, vars in settingsPlugins.items() %}
|
||||
<div class="tab-pane" id="settings_plugin_{{ plugin_name }}" data-bind="allowBindings: false">
|
||||
{% include plugin_name+"_settings_dialog.jinja2" %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="tab-pane" id="settings_logs" data-bind="allowBindings: false">
|
||||
<div id="logs">
|
||||
<h1>{{ _('Logs') }}</h1>
|
||||
|
|
|
|||
66
tests/test_plugin_core.py
Normal file
66
tests/test_plugin_core.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import unittest
|
||||
|
||||
import octoprint.plugin
|
||||
import octoprint.plugin.core
|
||||
|
||||
|
||||
class PluginTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
import os
|
||||
plugin_folders = [os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_plugins")]
|
||||
plugin_types = [octoprint.plugin.SettingsPlugin, octoprint.plugin.StartupPlugin]
|
||||
self.plugin_manager = octoprint.plugin.core.PluginManager(plugin_folders, plugin_types)
|
||||
|
||||
def test_plugin_loading(self):
|
||||
self.assertEquals(4, len(self.plugin_manager.plugins))
|
||||
self.assertEquals(1, len(self.plugin_manager.plugin_hooks))
|
||||
self.assertEquals(2, len(self.plugin_manager.plugin_implementations))
|
||||
|
||||
self.assertTrue("octoprint.core.startup" in self.plugin_manager.plugin_hooks)
|
||||
self.assertEquals(1, len(self.plugin_manager.plugin_hooks["octoprint.core.startup"]))
|
||||
|
||||
self.assertTrue(octoprint.plugin.StartupPlugin in self.plugin_manager.plugin_implementations)
|
||||
self.assertEquals(2, len(self.plugin_manager.plugin_implementations[octoprint.plugin.StartupPlugin]))
|
||||
|
||||
self.assertTrue(octoprint.plugin.SettingsPlugin in self.plugin_manager.plugin_implementations)
|
||||
self.assertEquals(2, len(self.plugin_manager.plugin_implementations[octoprint.plugin.SettingsPlugin]))
|
||||
|
||||
def test_get_plugin(self):
|
||||
plugin = self.plugin_manager.get_plugin("hook_plugin")
|
||||
self.assertIsNotNone(plugin)
|
||||
self.assertEquals("Hook Plugin", plugin.__plugin_name__)
|
||||
|
||||
plugin = self.plugin_manager.get_plugin("mixed_plugin")
|
||||
self.assertIsNotNone(plugin)
|
||||
self.assertEquals("Mixed Plugin", plugin.__plugin_name__)
|
||||
|
||||
plugin = self.plugin_manager.get_plugin("unknown_plugin")
|
||||
self.assertIsNone(plugin)
|
||||
|
||||
def test_get_hooks(self):
|
||||
hooks = self.plugin_manager.get_hooks("octoprint.core.startup")
|
||||
self.assertEquals(1, len(hooks))
|
||||
self.assertTrue("hook_plugin" in hooks)
|
||||
self.assertEquals("success", hooks["hook_plugin"]())
|
||||
|
||||
hooks = self.plugin_manager.get_hooks("octoprint.printing.print")
|
||||
self.assertEquals(0, len(hooks))
|
||||
|
||||
def test_get_implementation(self):
|
||||
implementations = self.plugin_manager.get_implementations(octoprint.plugin.StartupPlugin)
|
||||
self.assertEquals(2, len(implementations))
|
||||
self.assertTrue('startup_plugin' in implementations)
|
||||
self.assertTrue('mixed_plugin' in implementations)
|
||||
|
||||
implementations = self.plugin_manager.get_implementations(octoprint.plugin.SettingsPlugin)
|
||||
self.assertEquals(2, len(implementations))
|
||||
self.assertTrue('settings_plugin' in implementations)
|
||||
self.assertTrue('mixed_plugin' in implementations)
|
||||
|
||||
implementations = self.plugin_manager.get_implementations(octoprint.plugin.StartupPlugin, octoprint.plugin.SettingsPlugin)
|
||||
self.assertEquals(1, len(implementations))
|
||||
self.assertTrue('mixed_plugin' in implementations)
|
||||
10
tests/test_plugins/hook_plugin.py
Normal file
10
tests/test_plugins/hook_plugin.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
|
||||
def hook_startup():
|
||||
return "success"
|
||||
|
||||
|
||||
__plugin_name__ = "Hook Plugin"
|
||||
__plugin_description__ = "Test hook plugin"
|
||||
__plugin_hooks__ = {'octoprint.core.startup': hook_startup}
|
||||
17
tests/test_plugins/mixed_plugin/__init__.py
Normal file
17
tests/test_plugins/mixed_plugin/__init__.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
import octoprint.plugin
|
||||
|
||||
|
||||
class TestMixedPlugin(octoprint.plugin.StartupPlugin, octoprint.plugin.SettingsPlugin):
|
||||
pass
|
||||
|
||||
|
||||
__plugin_name__ = "Mixed Plugin"
|
||||
__plugin_description__ = "Test mixed plugin"
|
||||
__plugin_implementations__ = (TestMixedPlugin(),)
|
||||
12
tests/test_plugins/settings_plugin.py
Normal file
12
tests/test_plugins/settings_plugin.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
import octoprint.plugin
|
||||
|
||||
|
||||
class TestSettingsPlugin(octoprint.plugin.SettingsPlugin):
|
||||
pass
|
||||
|
||||
|
||||
__plugin_name__ = "Settings Plugin"
|
||||
__plugin_description__ = "Test settings plugin"
|
||||
__plugin_implementations__ = (TestSettingsPlugin(),)
|
||||
12
tests/test_plugins/startup_plugin.py
Normal file
12
tests/test_plugins/startup_plugin.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
import octoprint.plugin
|
||||
|
||||
|
||||
class TestStartupPlugin(octoprint.plugin.StartupPlugin):
|
||||
pass
|
||||
|
||||
|
||||
__plugin_name__ = "Startup Plugin"
|
||||
__plugin_description__ = "Test startup plugin"
|
||||
__plugin_implementations__ = (TestStartupPlugin(),)
|
||||
Loading…
Reference in a new issue