First experiments with a plugin system, service discovery and connectivity ensurance

This commit is contained in:
Gina Häußge 2014-09-05 17:11:11 +02:00
parent 85c170712a
commit e1366ef90f
30 changed files with 1362 additions and 151 deletions

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

View 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

View 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

View 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

View 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

View file

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

View 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")]);
});

View file

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

View file

@ -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">&times;</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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -120,4 +120,8 @@ function ConnectionViewModel(loginStateViewModel, settingsViewModel) {
})
}
}
self.onStartup = function() {
self.requestData();
};
}

View file

@ -236,6 +236,9 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
default:
return "customControls_emptyTemplate";
}
}
};
self.onStartup = function() {
self.requestData();
};
}

View file

@ -304,5 +304,12 @@ function GcodeFilesViewModel(printerStateViewModel, loginStateViewModel) {
}
};
self.onDataUpdaterReconnect = function() {
self.requestData();
};
self.onStartup = function() {
self.requestData();
};
}

View file

@ -405,4 +405,8 @@ function GcodeViewModel(loginStateViewModel, settingsViewModel) {
GCODE.ui.changeSelectedCommands(self.layerSlider.slider("getValue"), tuple[0], tuple[1]);
};
self.onDataUpdaterReconnect = function() {
self.reset();
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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}

View 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(),)

View 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(),)

View 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(),)