def __getattr__(self, item): all_access_methods = list(self.access_methods.keys()) + list( self.deprecated_access_methods.keys()) if item in all_access_methods: decorator = None if item in self.deprecated_access_methods: new = self.deprecated_access_methods[item] decorator = deprecated( "{old} has been renamed to {new}".format(old=item, new=new), stacklevel=2) item = new settings_name, args_mapper, kwargs_mapper = self.access_methods[ item] if hasattr(self.settings, settings_name) and callable( getattr(self.settings, settings_name)): orig_func = getattr(self.settings, settings_name) if decorator is not None: orig_func = decorator(orig_func) def _func(*args, **kwargs): return orig_func(*args_mapper(args), **kwargs_mapper(kwargs)) _func.__name__ = to_native_str(item) _func.__doc__ = orig_func.__doc__ if "__doc__" in dir( orig_func) else None return _func return getattr(self.settings, item)
def __getattr__(self, item): all_access_methods = self.access_methods.keys() + self.deprecated_access_methods.keys() if item in all_access_methods: decorator = None if item in self.deprecated_access_methods: new = self.deprecated_access_methods[item] decorator = deprecated("{old} has been renamed to {new}".format(old=item, new=new), stacklevel=2) item = new settings_name, args_mapper, kwargs_mapper = self.access_methods[item] if hasattr(self.settings, settings_name) and callable(getattr(self.settings, settings_name)): orig_func = getattr(self.settings, settings_name) if decorator is not None: orig_func = decorator(orig_func) def _func(*args, **kwargs): return orig_func(*args_mapper(args), **kwargs_mapper(kwargs)) _func.__name__ = item _func.__doc__ = orig_func.__doc__ if "__doc__" in dir(orig_func) else None return _func return getattr(self.settings, item)
return if _flask.request.endpoint and (_flask.request.endpoint == "static" or _flask.request.endpoint.endswith(".static")): # no further handling for static resources return apikey = get_api_key(_flask.request) if apikey is None: return _flask.make_response("No API key provided", 401) if apikey != octoprint.server.UI_API_KEY and not settings().getBoolean(["api", "enabled"]): # api disabled => 401 return _flask.make_response("API disabled", 401) apiKeyRequestHandler = deprecated("apiKeyRequestHandler has been renamed to enforceApiKeyRequestHandler")(enforceApiKeyRequestHandler) def loginFromApiKeyRequestHandler(): """ ``before_request`` handler for blueprints which creates a login session for the provided api key (if available) UI_API_KEY and app session keys are handled as anonymous keys here and ignored. """ apikey = get_api_key(_flask.request) if apikey and apikey != octoprint.server.UI_API_KEY and not octoprint.server.appSessionManager.validate(apikey): user = get_user_for_apikey(apikey) if user is not None and _flask.ext.login.login_user(user, remember=False): _flask.ext.principal.identity_changed.send(_flask.current_app._get_current_object(),
from . import flask from . import sockjs from . import tornado from . import watchdog @deprecated( "API keys are no longer needed for anonymous access and thus this is now obsolete" ) def enforceApiKeyRequestHandler(): pass apiKeyRequestHandler = deprecated( "apiKeyRequestHandler has been renamed to enforceApiKeyRequestHandler")( enforceApiKeyRequestHandler) def loginFromApiKeyRequestHandler(): """ ``before_request`` handler for blueprints which creates a login session for the provided api key (if available) App session keys are handled as anonymous keys here and ignored. """ try: if loginUserFromApiKey(): _flask.g.login_via_apikey = True except InvalidApiKeyException: return _flask.make_response("Invalid API key", 403)
class PluginSettings(object): """ The :class:`PluginSettings` class is the interface for plugins to their own or globally defined settings. It provides a couple of convenience methods for directly accessing plugin settings via the regular :class:`octoprint.settings.Settings` interfaces as well as means to access plugin specific folder locations. All getter and setter methods will ensure that plugin settings are stored in their correct location within the settings structure by modifying the supplied paths accordingly. Arguments: settings (Settings): The :class:`~octoprint.settings.Settings` instance on which to operate. plugin_key (str): The plugin identifier of the plugin for which to create this instance. defaults (dict): The plugin's defaults settings, will be used to determine valid paths within the plugin's settings structure .. method:: get(path, merged=False, asdict=False) Retrieves a raw value from the settings for ``path``, optionally merging the raw value with the default settings if ``merged`` is set to True. :param path: The path for which to retrieve the value. :type path: list, tuple :param boolean merged: Whether to merge the returned result with the default settings (True) or not (False, default). :returns: The retrieved settings value. :rtype: object .. method:: get_int(path) Like :func:`get` but tries to convert the retrieved value to ``int``. .. method:: get_float(path) Like :func:`get` but tries to convert the retrieved value to ``float``. .. method:: get_boolean(path) Like :func:`get` but tries to convert the retrieved value to ``boolean``. .. method:: set(path, value, force=False) Sets the raw value on the settings for ``path``. :param path: The path for which to retrieve the value. :type path: list, tuple :param object value: The value to set. :param boolean force: If set to True, the modified configuration will even be written back to disk if the value didn't change. .. method:: set_int(path, value, force=False) Like :func:`set` but ensures the value is an ``int`` through attempted conversion before setting it. .. method:: set_float(path, value, force=False) Like :func:`set` but ensures the value is an ``float`` through attempted conversion before setting it. .. method:: set_boolean(path, value, force=False) Like :func:`set` but ensures the value is an ``boolean`` through attempted conversion before setting it. """ def __init__(self, settings, plugin_key, defaults=None, get_preprocessors=None, set_preprocessors=None): self.settings = settings self.plugin_key = plugin_key if defaults is not None: self.defaults = dict(plugins=dict()) self.defaults["plugins"][plugin_key] = defaults self.defaults["plugins"][plugin_key]["_config_version"] = None else: self.defaults = None if get_preprocessors is None: get_preprocessors = dict() self.get_preprocessors = dict(plugins=dict()) self.get_preprocessors["plugins"][plugin_key] = get_preprocessors if set_preprocessors is None: set_preprocessors = dict() self.set_preprocessors = dict(plugins=dict()) self.set_preprocessors["plugins"][plugin_key] = set_preprocessors def prefix_path_in_args(args, index=0): result = [] if index == 0: result.append(self._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(self._prefix_path(args[index])) result.extend(args_after) return result def add_getter_kwargs(kwargs): if not "defaults" in kwargs and self.defaults is not None: kwargs.update(defaults=self.defaults) if not "preprocessors" in kwargs: kwargs.update(preprocessors=self.get_preprocessors) return kwargs def add_setter_kwargs(kwargs): if not "defaults" in kwargs and self.defaults is not None: kwargs.update(defaults=self.defaults) if not "preprocessors" in kwargs: kwargs.update(preprocessors=self.set_preprocessors) return kwargs self.access_methods = dict( has=("has", prefix_path_in_args, add_getter_kwargs), get=("get", prefix_path_in_args, add_getter_kwargs), get_int=("getInt", prefix_path_in_args, add_getter_kwargs), get_float=("getFloat", prefix_path_in_args, add_getter_kwargs), get_boolean=("getBoolean", prefix_path_in_args, add_getter_kwargs), set=("set", prefix_path_in_args, add_setter_kwargs), set_int=("setInt", prefix_path_in_args, add_setter_kwargs), set_float=("setFloat", prefix_path_in_args, add_setter_kwargs), set_boolean=("setBoolean", prefix_path_in_args, add_setter_kwargs), remove=("remove", prefix_path_in_args, lambda x: x)) self.deprecated_access_methods = dict(getInt="get_int", getFloat="get_float", getBoolean="get_boolean", setInt="set_int", setFloat="set_float", setBoolean="set_boolean") def _prefix_path(self, path=None): if path is None: path = list() return ['plugins', self.plugin_key] + path def global_has(self, path, **kwargs): return self.settings.has(path, **kwargs) def global_remove(self, path, **kwargs): return self.settings.remove(path, **kwargs) def global_get(self, path, **kwargs): """ Getter for retrieving settings not managed by the plugin itself from the core settings structure. Use this to access global settings outside of your plugin. Directly forwards to :func:`octoprint.settings.Settings.get`. """ return self.settings.get(path, **kwargs) def global_get_int(self, path, **kwargs): """ Like :func:`global_get` but directly forwards to :func:`octoprint.settings.Settings.getInt`. """ return self.settings.getInt(path, **kwargs) def global_get_float(self, path, **kwargs): """ Like :func:`global_get` but directly forwards to :func:`octoprint.settings.Settings.getFloat`. """ return self.settings.getFloat(path, **kwargs) def global_get_boolean(self, path, **kwargs): """ Like :func:`global_get` but directly orwards to :func:`octoprint.settings.Settings.getBoolean`. """ return self.settings.getBoolean(path, **kwargs) def global_set(self, path, value, **kwargs): """ Setter for modifying settings not managed by the plugin itself on the core settings structure. Use this to modify global settings outside of your plugin. Directly forwards to :func:`octoprint.settings.Settings.set`. """ self.settings.set(path, value, **kwargs) def global_set_int(self, path, value, **kwargs): """ Like :func:`global_set` but directly forwards to :func:`octoprint.settings.Settings.setInt`. """ self.settings.setInt(path, value, **kwargs) def global_set_float(self, path, value, **kwargs): """ Like :func:`global_set` but directly forwards to :func:`octoprint.settings.Settings.setFloat`. """ self.settings.setFloat(path, value, **kwargs) def global_set_boolean(self, path, value, **kwargs): """ Like :func:`global_set` but directly forwards to :func:`octoprint.settings.Settings.setBoolean`. """ self.settings.setBoolean(path, value, **kwargs) def global_get_basefolder(self, folder_type, **kwargs): """ Retrieves a globally defined basefolder of the given ``folder_type``. Directly forwards to :func:`octoprint.settings.Settings.getBaseFolder`. """ return self.settings.getBaseFolder(folder_type, **kwargs) def get_plugin_logfile_path(self, postfix=None): """ Retrieves the path to a logfile specifically for the plugin. If ``postfix`` is not supplied, the logfile will be named ``plugin_<plugin identifier>.log`` and located within the configured ``logs`` folder. If a postfix is supplied, the name will be ``plugin_<plugin identifier>_<postfix>.log`` at the same location. Plugins may use this for specific logging tasks. For example, a :class:`~octoprint.plugin.SlicingPlugin` might want to create a log file for logging the output of the slicing engine itself if some debug flag is set. Arguments: postfix (str): Postfix of the logfile for which to create the path. If set, the file name of the log file will be ``plugin_<plugin identifier>_<postfix>.log``, if not it will be ``plugin_<plugin identifier>.log``. Returns: str: Absolute path to the log file, directly usable by the plugin. """ filename = "plugin_" + self.plugin_key if postfix is not None: filename += "_" + postfix filename += ".log" return os.path.join(self.settings.getBaseFolder("logs"), filename) @deprecated( "PluginSettings.get_plugin_data_folder has been replaced by OctoPrintPlugin.get_plugin_data_folder", includedoc= "Replaced by :func:`~octoprint.plugin.types.OctoPrintPlugin.get_plugin_data_folder`", since="1.2.0") def get_plugin_data_folder(self): path = os.path.join(self.settings.getBaseFolder("data"), self.plugin_key) if not os.path.isdir(path): os.makedirs(path) return path def get_all_data(self, **kwargs): merged = kwargs.get("merged", True) asdict = kwargs.get("asdict", True) defaults = kwargs.get("defaults", self.defaults) preprocessors = kwargs.get("preprocessors", self.get_preprocessors) kwargs.update( dict(merged=merged, asdict=asdict, defaults=defaults, preprocessors=preprocessors)) return self.settings.get(self._prefix_path(), **kwargs) def clean_all_data(self): self.settings.remove(self._prefix_path()) def __getattr__(self, item): all_access_methods = self.access_methods.keys( ) + self.deprecated_access_methods.keys() if item in all_access_methods: decorator = None if item in self.deprecated_access_methods: new = self.deprecated_access_methods[item] decorator = deprecated( "{old} has been renamed to {new}".format(old=item, new=new), stacklevel=2) item = new settings_name, args_mapper, kwargs_mapper = self.access_methods[ item] if hasattr(self.settings, settings_name) and callable( getattr(self.settings, settings_name)): orig_func = getattr(self.settings, settings_name) if decorator is not None: orig_func = decorator(orig_func) def _func(*args, **kwargs): return orig_func(*args_mapper(args), **kwargs_mapper(kwargs)) _func.__name__ = item _func.__doc__ = orig_func.__doc__ if "__doc__" in dir( orig_func) else None return _func return getattr(self.settings, item) ##~~ deprecated methods follow # TODO: Remove with release of 1.3.0 globalGet = deprecated("globalGet has been renamed to global_get", includedoc="Replaced by :func:`global_get`", since="1.2.0-dev-546")(global_get) globalGetInt = deprecated( "globalGetInt has been renamed to global_get_int", includedoc="Replaced by :func:`global_get_int`", since="1.2.0-dev-546")(global_get_int) globalGetFloat = deprecated( "globalGetFloat has been renamed to global_get_float", includedoc="Replaced by :func:`global_get_float`", since="1.2.0-dev-546")(global_get_float) globalGetBoolean = deprecated( "globalGetBoolean has been renamed to global_get_boolean", includedoc="Replaced by :func:`global_get_boolean`", since="1.2.0-dev-546")(global_get_boolean) globalSet = deprecated("globalSet has been renamed to global_set", includedoc="Replaced by :func:`global_set`", since="1.2.0-dev-546")(global_set) globalSetInt = deprecated( "globalSetInt has been renamed to global_set_int", includedoc="Replaced by :func:`global_set_int`", since="1.2.0-dev-546")(global_set_int) globalSetFloat = deprecated( "globalSetFloat has been renamed to global_set_float", includedoc="Replaced by :func:`global_set_float`", since="1.2.0-dev-546")(global_set_float) globalSetBoolean = deprecated( "globalSetBoolean has been renamed to global_set_boolean", includedoc="Replaced by :func:`global_set_boolean`", since="1.2.0-dev-546")(global_set_boolean) globalGetBaseFolder = deprecated( "globalGetBaseFolder has been renamed to global_get_basefolder", includedoc="Replaced by :func:`global_get_basefolder`", since="1.2.0-dev-546")(global_get_basefolder) getPluginLogfilePath = deprecated( "getPluginLogfilePath has been renamed to get_plugin_logfile_path", includedoc="Replaced by :func:`get_plugin_logfile_path`", since="1.2.0-dev-546")(get_plugin_logfile_path)
class FilebasedUserManager(UserManager): def __init__(self, group_manager, path=None, settings=None): UserManager.__init__(self, group_manager, settings=settings) if path is None: path = self._settings.get(["accessControl", "userfile"]) if path is None: path = os.path.join(s().getBaseFolder("base"), "users.yaml") self._userfile = path self._users = {} self._dirty = False self._customized = None self._load() def _load(self): if os.path.exists(self._userfile) and os.path.isfile(self._userfile): self._customized = True with io.open(self._userfile, 'rt', encoding='utf-8') as f: data = yaml.safe_load(f) for name, attributes in data.items(): if not isinstance(attributes, dict): continue permissions = [] if "permissions" in attributes: permissions = attributes["permissions"] if "groups" in attributes: groups = set(attributes["groups"]) else: groups = {self._group_manager.user_group} # migrate from roles to permissions if "roles" in attributes and not "permissions" in attributes: self._logger.info( "Migrating user {} to new granular permission system" .format(name)) groups |= set( self._migrate_roles_to_groups(attributes["roles"])) self._dirty = True apikey = None if "apikey" in attributes: apikey = attributes["apikey"] settings = dict() if "settings" in attributes: settings = attributes["settings"] self._users[name] = User( username=name, passwordHash=attributes["password"], active=attributes["active"], permissions=self._to_permissions(*permissions), groups=self._to_groups(*groups), apikey=apikey, settings=settings) for sessionid in self._sessionids_by_userid.get( name, set()): if sessionid in self._session_users_by_session: self._session_users_by_session[ sessionid].update_user(self._users[name]) if self._dirty: self._save() else: self._customized = False def _save(self, force=False): if not self._dirty and not force: return data = {} for name, user in self._users.items(): if not user or not isinstance(user, User): continue data[name] = { "password": user._passwordHash, "active": user._active, "groups": self._from_groups(*user._groups), "permissions": self._from_permissions(*user._permissions), "apikey": user._apikey, "settings": user._settings, # TODO: deprecated, remove in 1.5.0 "roles": user._roles } with atomic_write(self._userfile, mode='wt', permissions=0o600, max_permissions=0o666) as f: yaml.safe_dump(data, f, default_flow_style=False, indent=4, allow_unicode=True) self._dirty = False self._load() def _migrate_roles_to_groups(self, roles): # If admin is inside the roles, just return admin group if "admin" in roles: return [ self._group_manager.admin_group, self._group_manager.user_group ] else: return [self._group_manager.user_group] def _refresh_groups(self, user): user._groups = self._to_groups(*map(lambda g: g.key, user.groups)) def add_user(self, username, password, active=False, permissions=None, groups=None, apikey=None, overwrite=False): if not permissions: permissions = [] permissions = self._to_permissions(*permissions) if not groups: groups = self._group_manager.default_groups groups = self._to_groups(*groups) if username in self._users and not overwrite: raise UserAlreadyExists(username) self._users[username] = User(username, UserManager.create_password_hash( password, settings=self._settings), active, permissions, groups, apikey=apikey) self._dirty = True self._save() def change_user_activation(self, username, active): if username not in self._users: raise UnknownUser(username) if self._users[username].is_active != active: self._users[username]._active = active self._dirty = True self._save() self._trigger_on_user_modified(username) def change_user_permissions(self, username, permissions): if username not in self._users: raise UnknownUser(username) user = self._users[username] permissions = self._to_permissions(*permissions) removed_permissions = list(set(user._permissions) - set(permissions)) added_permissions = list(set(permissions) - set(user._permissions)) if len(removed_permissions) > 0: user.remove_permissions_from_user(removed_permissions) self._dirty = True if len(added_permissions) > 0: user.add_permissions_to_user(added_permissions) self._dirty = True if self._dirty: self._save() self._trigger_on_user_modified(username) def add_permissions_to_user(self, username, permissions): if username not in self._users: raise UnknownUser(username) if self._users[username].add_permissions_to_user( self._to_permissions(*permissions)): self._dirty = True self._save() self._trigger_on_user_modified(username) def remove_permissions_from_user(self, username, permissions): if username not in self._users: raise UnknownUser(username) if self._users[username].remove_permissions_from_user( self._to_permissions(*permissions)): self._dirty = True self._save() self._trigger_on_user_modified(username) def remove_permissions_from_users(self, permissions): modified = [] for user in self._users: dirty = user.remove_permissions_from_user( self._to_permissions(*permissions)) if dirty: self._dirty = True modified.append(user.get_id()) if self._dirty: self._save() for username in modified: self._trigger_on_user_modified(username) def change_user_groups(self, username, groups): if username not in self._users: raise UnknownUser(username) user = self._users[username] groups = self._to_groups(*groups) removed_groups = list(set(user._groups) - set(groups)) added_groups = list(set(groups) - set(user._groups)) if len(removed_groups): self._dirty |= user.remove_groups_from_user(removed_groups) if len(added_groups): self._dirty |= user.add_groups_to_user(added_groups) if self._dirty: self._save() self._trigger_on_user_modified(username) def add_groups_to_user(self, username, groups, save=True, notify=True): if username not in self._users: raise UnknownUser(username) if self._users[username].add_groups_to_user(self._to_groups(*groups)): self._dirty = True if save: self._save() if notify: self._trigger_on_user_modified(username) def remove_groups_from_user(self, username, groups, save=True, notify=True): if username not in self._users: raise UnknownUser(username) if self._users[username].remove_groups_from_user( self._to_groups(*groups)): self._dirty = True if save: self._save() if notify: self._trigger_on_user_modified(username) def remove_groups_from_users(self, groups): modified = [] for username, user in self._users.items(): dirty = user.remove_groups_from_user(self._to_groups(*groups)) if dirty: self._dirty = True modified.append(username) if self._dirty: self._save() for username in modified: self._trigger_on_user_modified(username) def change_user_password(self, username, password): if not username in self._users: raise UnknownUser(username) passwordHash = UserManager.create_password_hash( password, settings=self._settings) user = self._users[username] if user._passwordHash != passwordHash: user._passwordHash = passwordHash self._dirty = True self._save() def change_user_setting(self, username, key, value): if not username in self._users: raise UnknownUser(username) user = self._users[username] old_value = user.get_setting(key) if not old_value or old_value != value: user.set_setting(key, value) self._dirty = self._dirty or old_value != value self._save() def change_user_settings(self, username, new_settings): if not username in self._users: raise UnknownUser(username) user = self._users[username] for key, value in new_settings.items(): old_value = user.get_setting(key) user.set_setting(key, value) self._dirty = self._dirty or old_value != value self._save() def get_all_user_settings(self, username): if not username in self._users: raise UnknownUser(username) user = self._users[username] return user.get_all_settings() def get_user_setting(self, username, key): if not username in self._users: raise UnknownUser(username) user = self._users[username] return user.get_setting(key) def generate_api_key(self, username): if not username in self._users: raise UnknownUser(username) user = self._users[username] user._apikey = generate_api_key() self._dirty = True self._save() return user._apikey def delete_api_key(self, username): if not username in self._users: raise UnknownUser(username) user = self._users[username] user._apikey = None self._dirty = True self._save() def remove_user(self, username): UserManager.remove_user(self, username) if not username in self._users: raise UnknownUser(username) del self._users[username] self._dirty = True self._save() def find_user(self, userid=None, apikey=None, session=None): user = UserManager.find_user(self, userid=userid, session=session) if user is not None: return user if userid is not None: if userid not in self._users: return None return self._users[userid] elif apikey is not None: for user in self._users.values(): if apikey == user._apikey: return user return None else: return None def get_all_users(self): return list(self._users.values()) def has_been_customized(self): return self._customized def on_group_permissions_changed(self, group, added=None, removed=None): # refresh our group references for user in self.get_all_users(): if group in user.groups: self._refresh_groups(user) # call parent UserManager.on_group_permissions_changed(self, group, added=added, removed=removed) def on_group_subgroups_changed(self, group, added=None, removed=None): # refresh our group references for user in self.get_all_users(): if group in user.groups: self._refresh_groups(user) # call parent UserManager.on_group_subgroups_changed(self, group, added=added, removed=removed) #~~ Helpers def _to_groups(self, *groups): return list( set( filter(lambda x: x is not None, (self._group_manager._to_group(group) for group in groups)))) def _to_permissions(self, *permissions): return list( set( filter(lambda x: x is not None, (Permissions.find(permission) for permission in permissions)))) def _from_groups(self, *groups): return list(set(group.key for group in groups)) def _from_permissions(self, *permissions): return list(set(permission.key for permission in permissions)) # ~~ Deprecated methods follow # TODO: Remove deprecated methods in OctoPrint 1.5.0 generateApiKey = deprecated( "generateApiKey has been renamed to generate_api_key", includedoc="Replaced by :func:`generate_api_key`", since="1.4.0")(generate_api_key) deleteApiKey = deprecated( "deleteApiKey has been renamed to delete_api_key", includedoc="Replaced by :func:`delete_api_key`", since="1.4.0")(delete_api_key) addUser = deprecated("addUser has been renamed to add_user", includedoc="Replaced by :func:`add_user`", since="1.4.0")(add_user) changeUserActivation = deprecated( "changeUserActivation has been renamed to change_user_activation", includedoc="Replaced by :func:`change_user_activation`", since="1.4.0")(change_user_activation) changeUserPassword = deprecated( "changeUserPassword has been renamed to change_user_password", includedoc="Replaced by :func:`change_user_password`", since="1.4.0")(change_user_password) getUserSetting = deprecated( "getUserSetting has been renamed to get_user_setting", includedoc="Replaced by :func:`get_user_setting`", since="1.4.0")(get_user_setting) getAllUserSettings = deprecated( "getAllUserSettings has been renamed to get_all_user_settings", includedoc="Replaced by :func:`get_all_user_settings`", since="1.4.0")(get_all_user_settings) changeUserSetting = deprecated( "changeUserSetting has been renamed to change_user_setting", includedoc="Replaced by :func:`change_user_setting`", since="1.4.0")(change_user_setting) changeUserSettings = deprecated( "changeUserSettings has been renamed to change_user_settings", includedoc="Replaced by :func:`change_user_settings`", since="1.4.0")(change_user_settings) removeUser = deprecated("removeUser has been renamed to remove_user", includedoc="Replaced by :func:`remove_user`", since="1.4.0")(remove_user) findUser = deprecated("findUser has been renamed to find_user", includedoc="Replaced by :func:`find_user`", since="1.4.0")(find_user) getAllUsers = deprecated("getAllUsers has been renamed to get_all_users", includedoc="Replaced by :func:`get_all_users`", since="1.4.0")(get_all_users) hasBeenCustomized = deprecated( "hasBeenCustomized has been renamed to has_been_customized", includedoc="Replaced by :func:`has_been_customized`", since="1.4.0")(has_been_customized)
class UserManager(GroupChangeListener, object): def __init__(self, group_manager, settings=None): self._group_manager = group_manager self._group_manager.register_listener(self) self._logger = logging.getLogger(__name__) self._session_users_by_session = dict() self._sessionids_by_userid = dict() self._enabled = True if settings is None: settings = s() self._settings = settings self._login_status_listeners = [] def register_login_status_listener(self, listener): self._login_status_listeners.append(listener) def unregister_login_status_listener(self, listener): self._login_status_listeners.remove(listener) def anonymous_user_factory(self): if self.enabled: return AnonymousUser([self._group_manager.guest_group]) else: return AdminUser([ self._group_manager.admin_group, self._group_manager.user_group ]) def api_user_factory(self): return ApiUser( [self._group_manager.admin_group, self._group_manager.user_group]) @property def enabled(self): return self._enabled @enabled.setter def enabled(self, value): self._enabled = value def enable(self): self._enabled = True def disable(self): self._enabled = False def login_user(self, user): self._cleanup_sessions() if user is None or user.is_anonymous: return if isinstance(user, LocalProxy): # noinspection PyProtectedMember user = user._get_current_object() if not isinstance(user, User): return None if not isinstance(user, SessionUser): user = SessionUser(user) self._session_users_by_session[user.session] = user userid = user.get_id() if not userid in self._sessionids_by_userid: self._sessionids_by_userid[userid] = set() self._sessionids_by_userid[userid].add(user.session) for listener in self._login_status_listeners: try: listener.on_user_logged_in(user) except Exception: self._logger.exception( "Error in on_user_logged_in on {!r}".format(listener), extra=dict(callback=fqcn(listener))) self._logger.info("Logged in user: {}".format(user.get_id())) return user def logout_user(self, user, stale=False): if user is None or user.is_anonymous or isinstance(user, AdminUser): return if isinstance(user, LocalProxy): user = user._get_current_object() if not isinstance(user, SessionUser): return userid = user.get_id() sessionid = user.session if userid in self._sessionids_by_userid: try: self._sessionids_by_userid[userid].remove(sessionid) except KeyError: pass if sessionid in self._session_users_by_session: try: del self._session_users_by_session[sessionid] except KeyError: pass for listener in self._login_status_listeners: try: listener.on_user_logged_out(user, stale=stale) except Exception: self._logger.exception( "Error in on_user_logged_out on {!r}".format(listener), extra=dict(callback=fqcn(listener))) self._logger.info("Logged out user: {}".format(user.get_id())) def _cleanup_sessions(self): for session, user in list(self._session_users_by_session.items()): if not isinstance(user, SessionUser): continue if user.created + (24 * 60 * 60) < monotonic_time(): self._logger.info( "Cleaning up user session {} for user {}".format( session, user.get_id())) self.logout_user(user, stale=True) @staticmethod def create_password_hash(password, salt=None, settings=None): if not salt: if settings is None: settings = s() salt = settings.get(["accessControl", "salt"]) if salt is None: import string from random import choice chars = string.ascii_lowercase + string.ascii_uppercase + string.digits salt = "".join(choice(chars) for _ in range(32)) settings.set(["accessControl", "salt"], salt) settings.save() return hashlib.sha512( to_bytes(password, encoding="utf-8", errors="replace") + to_bytes(salt)).hexdigest() def check_password(self, username, password): user = self.find_user(username) if not user: return False hash = UserManager.create_password_hash(password, settings=self._settings) if user.check_password(hash): # new hash matches, correct password return True else: # new hash doesn't match, but maybe the old one does, so check that! oldHash = UserManager.create_password_hash( password, salt="mvBUTvwzBzD3yPwvnJ4E4tXNf3CGJvvW", settings=self._settings) if user.check_password(oldHash): # old hash matches, we migrate the stored password hash to the new one and return True since it's the correct password self.change_user_password(username, password) return True else: # old hash doesn't match either, wrong password return False def add_user(self, username, password, active, permissions, groups, overwrite=False): pass def change_user_activation(self, username, active): pass def change_user_permissions(self, username, permissions): pass def add_permissions_to_user(self, username, permissions): pass def remove_permissions_from_user(self, username, permissions): pass def change_user_groups(self, username, groups): pass def add_groups_to_user(self, username, groups): pass def remove_groups_from_user(self, username, groups): pass def remove_groups_from_users(self, group): pass def change_user_password(self, username, password): pass def get_user_setting(self, username, key): return None def get_all_user_settings(self, username): return dict() def change_user_setting(self, username, key, value): pass def change_user_settings(self, username, new_settings): pass def remove_user(self, username): if username in self._sessionids_by_userid: sessions = self._sessionids_by_userid[username] for session in sessions: if session in self._session_users_by_session: del self._session_users_by_session[session] del self._sessionids_by_userid[username] def validate_user_session(self, userid, session): if session in self._session_users_by_session: user = self._session_users_by_session[session] return userid == user.get_id() return False def find_user(self, userid=None, session=None): if session is not None and session in self._session_users_by_session: user = self._session_users_by_session[session] if userid is None or userid == user.get_id(): return user return None def find_sessions_for(self, matcher): result = [] for user in self.get_all_users(): if matcher(user): try: session_ids = self._sessionids_by_userid[user.get_id()] for session_id in session_ids: try: result.append( self._session_users_by_session[session_id]) except KeyError: # unknown session after all continue except KeyError: # no session for user pass return result def get_all_users(self): return [] def has_been_customized(self): return False def on_group_removed(self, group): self._logger.debug( "Group {} got removed, removing from all users".format(group.key)) self.remove_groups_from_users([group]) def on_group_permissions_changed(self, group, added=None, removed=None): users = self.find_sessions_for(lambda u: group in u.groups) for listener in self._login_status_listeners: try: for user in users: listener.on_user_modified(user) except Exception: self._logger.exception( "Error in on_user_modified on {!r}".format(listener), extra=dict(callback=fqcn(listener))) def on_group_subgroups_changed(self, group, added=None, removed=None): users = self.find_sessions_for(lambda u: group in u.groups) for listener in self._login_status_listeners: # noinspection PyBroadException try: for user in users: listener.on_user_modified(user) except Exception: self._logger.exception( "Error in on_user_modified on {!r}".format(listener), extra=dict(callback=fqcn(listener))) def _trigger_on_user_modified(self, user): if isinstance(user, basestring): # user id users = [] try: session_ids = self._sessionids_by_userid[user] for session_id in session_ids: try: users.append( self._session_users_by_session[session_id]) except KeyError: # unknown session id continue except KeyError: # no session for user return elif isinstance(user, User) and not isinstance(user, SessionUser): users = self.find_sessions_for( lambda u: u.get_id() == user.get_id()) elif isinstance(user, User): users = [user] else: return for listener in self._login_status_listeners: try: for user in users: listener.on_user_modified(user) except Exception: self._logger.exception( "Error in on_user_modified on {!r}".format(listener), extra=dict(callback=fqcn(listener))) def _trigger_on_user_removed(self, username): for listener in self._login_status_listeners: try: listener.on_user_removed(username) except Exception: self._logger.exception( "Error in on_user_removed on {!r}".format(listener), extra=dict(callback=fqcn(listener))) #~~ Deprecated methods follow # TODO: Remove deprecated methods in OctoPrint 1.5.0 @staticmethod def createPasswordHash(*args, **kwargs): """ .. deprecated: 1.4.0 Replaced by :func:`~UserManager.create_password_hash` """ # we can't use the deprecated decorator here since this method is static import warnings warnings.warn( "createPasswordHash has been renamed to create_password_hash", DeprecationWarning, stacklevel=2) return UserManager.create_password_hash(*args, **kwargs) @deprecated("changeUserRoles has been replaced by change_user_permissions", includedoc="Replaced by :func:`change_user_permissions`", since="1.4.0") def changeUserRoles(self, username, roles): user = self.find_user(username) if user is None: raise UnknownUser(username) removed_roles = set(user._roles) - set(roles) self.removeRolesFromUser(username, removed_roles, user=user) added_roles = set(roles) - set(user._roles) self.addRolesToUser(username, added_roles, user=user) @deprecated("addRolesToUser has been replaced by add_permissions_to_user", includedoc="Replaced by :func:`add_permissions_to_user`", since="1.4.0") def addRolesToUser(self, username, roles, user=None): if user is None: user = self.find_user(username) if user is None: raise UnknownUser(username) if "admin" in roles: self.add_groups_to_user(username, self._group_manager.admin_group) if "user" in roles: self.remove_groups_from_user(username, self._group_manager.user_group) @deprecated( "removeRolesFromUser has been replaced by remove_permissions_from_user", includedoc="Replaced by :func:`remove_permissions_from_user`", since="1.4.0") def removeRolesFromUser(self, username, roles, user=None): if user is None: user = self.find_user(username) if user is None: raise UnknownUser(username) if "admin" in roles: self.remove_groups_from_user(username, self._group_manager.admin_group) self.remove_permissions_from_user(username, Permissions.ADMIN) if "user" in roles: self.remove_groups_from_user(username, self._group_manager.user_group) checkPassword = deprecated( "checkPassword has been renamed to check_password", includedoc="Replaced by :func:`check_password`", since="1.4.0")(check_password) addUser = deprecated("addUser has been renamed to add_user", includedoc="Replaced by :func:`add_user`", since="1.4.0")(add_user) changeUserActivation = deprecated( "changeUserActivation has been renamed to change_user_activation", includedoc="Replaced by :func:`change_user_activation`", since="1.4.0")(change_user_activation) changeUserPassword = deprecated( "changeUserPassword has been renamed to change_user_password", includedoc="Replaced by :func:`change_user_password`", since="1.4.0")(change_user_password) getUserSetting = deprecated( "getUserSetting has been renamed to get_user_setting", includedoc="Replaced by :func:`get_user_setting`", since="1.4.0")(get_user_setting) getAllUserSettings = deprecated( "getAllUserSettings has been renamed to get_all_user_settings", includedoc="Replaced by :func:`get_all_user_settings`", since="1.4.0")(get_all_user_settings) changeUserSetting = deprecated( "changeUserSetting has been renamed to change_user_setting", includedoc="Replaced by :func:`change_user_setting`", since="1.4.0")(change_user_setting) changeUserSettings = deprecated( "changeUserSettings has been renamed to change_user_settings", includedoc="Replaced by :func:`change_user_settings`", since="1.4.0")(change_user_settings) removeUser = deprecated("removeUser has been renamed to remove_user", includedoc="Replaced by :func:`remove_user`", since="1.4.0")(remove_user) findUser = deprecated("findUser has been renamed to find_user", includedoc="Replaced by :func:`find_user`", since="1.4.0")(find_user) getAllUsers = deprecated("getAllUsers has been renamed to get_all_users", includedoc="Replaced by :func:`get_all_users`", since="1.4.0")(get_all_users) hasBeenCustomized = deprecated( "hasBeenCustomized has been renamed to has_been_customized", includedoc="Replaced by :func:`has_been_customized`", since="1.4.0")(has_been_customized)
class User(UserMixin): def __init__(self, username, passwordHash, active, permissions=None, groups=None, apikey=None, settings=None): if permissions is None: permissions = [] if groups is None: groups = [] self._username = username self._passwordHash = passwordHash self._active = active self._permissions = permissions self._groups = groups self._apikey = apikey if settings is None: settings = dict() self._settings = settings def as_dict(self): from octoprint.access.permissions import OctoPrintPermission return { "name": self._username, "active": bool(self.is_active), "permissions": list(map(lambda p: p.key, self._permissions)), "groups": list(map(lambda g: g.key, self._groups)), "needs": OctoPrintPermission.convert_needs_to_dict(self.needs), "apikey": self._apikey, "settings": self._settings, # TODO: deprecated, remove in 1.5.0 "admin": self.has_permission(Permissions.ADMIN), "user": not self.is_anonymous, "roles": self._roles } def check_password(self, passwordHash): return self._passwordHash == passwordHash def get_id(self): return self.get_name() def get_name(self): return self._username @property def is_anonymous(self): return FlaskLoginMethodReplacedByBooleanProperty( "is_anonymous", lambda: False) @property def is_authenticated(self): return FlaskLoginMethodReplacedByBooleanProperty( "is_authenticated", lambda: True) @property def is_active(self): return FlaskLoginMethodReplacedByBooleanProperty( "is_active", lambda: self._active) def get_all_settings(self): return self._settings def get_setting(self, key): if not isinstance(key, (tuple, list)): path = [key] else: path = key return self._get_setting(path) def set_setting(self, key, value): if not isinstance(key, (tuple, list)): path = [key] else: path = key return self._set_setting(path, value) def _get_setting(self, path): s = self._settings for p in path: if isinstance(s, dict) and p in s: s = s[p] else: return None return s def _set_setting(self, path, value): s = self._settings for p in path[:-1]: if p not in s: s[p] = dict() if not isinstance(s[p], dict): s[p] = dict() s = s[p] key = path[-1] s[key] = value return True def add_permissions_to_user(self, permissions): # Make sure the permissions variable is of type list if not isinstance(permissions, list): permissions = [permissions] assert (all( map(lambda p: isinstance(p, OctoPrintPermission), permissions))) dirty = False for permission in permissions: if permissions not in self._permissions: self._permissions.append(permission) dirty = True return dirty def remove_permissions_from_user(self, permissions): # Make sure the permissions variable is of type list if not isinstance(permissions, list): permissions = [permissions] assert (all( map(lambda p: isinstance(p, OctoPrintPermission), permissions))) dirty = False for permission in permissions: if permission in self._permissions: self._permissions.remove(permission) dirty = True return dirty def add_groups_to_user(self, groups): # Make sure the groups variable is of type list if not isinstance(groups, list): groups = [groups] assert (all(map(lambda p: isinstance(p, Group), groups))) dirty = False for group in groups: if group.is_toggleable() and group not in self._groups: self._groups.append(group) dirty = True return dirty def remove_groups_from_user(self, groups): # Make sure the groups variable is of type list if not isinstance(groups, list): groups = [groups] assert (all(map(lambda p: isinstance(p, Group), groups))) dirty = False for group in groups: if group.is_toggleable() and group in self._groups: self._groups.remove(group) dirty = True return dirty @property def permissions(self): if self._permissions is None: return [] if Permissions.ADMIN in self._permissions: return Permissions.all() return list(filter(lambda p: p is not None, self._permissions)) @property def groups(self): return list(self._groups) @property def effective_permissions(self): if self._permissions is None: return [] return list( filter(lambda p: p is not None and self.has_permission(p), Permissions.all())) @property def needs(self): needs = set() for permission in self.permissions: if permission is not None: needs = needs.union(permission.needs) for group in self.groups: if group is not None: needs = needs.union(group.needs) return needs def has_permission(self, permission): return self.has_needs(*permission.needs) def has_needs(self, *needs): return set(needs).issubset(self.needs) def __repr__(self): return "User(id=%s,name=%s,active=%r,user=True,admin=%r,permissions=%s,groups=%s)" % ( self.get_id(), self.get_name(), bool( self.is_active), self.has_permission( Permissions.ADMIN), self._permissions, self._groups) # ~~ Deprecated methods & properties follow # TODO: Remove deprecated methods & properties in OctoPrint 1.5.0 asDict = deprecated("asDict has been renamed to as_dict", includedoc="Replaced by :func:`as_dict`", since="1.4.0")(as_dict) @property @deprecated("is_user is deprecated, please use has_permission", since="1.4.0") def is_user(self): return OctoPrintUserMethodReplacedByBooleanProperty( "is_user", lambda: not self.is_anonymous) @property @deprecated("is_admin is deprecated, please use has_permission", since="1.4.0") def is_admin(self): return OctoPrintUserMethodReplacedByBooleanProperty( "is_admin", lambda: self.has_permission(Permissions.ADMIN)) @property @deprecated("roles is deprecated, please use has_permission", since="1.4.0") def roles(self): return self._roles @property def _roles(self): """Helper for the deprecated self.roles and serializing to yaml""" if self.has_permission(Permissions.ADMIN): return ["user", "admin"] elif not self.is_anonymous: return ["user"] else: return []
class Printer(PrinterInterface, comm.MachineComPrintCallback): """ Default implementation of the :class:`PrinterInterface`. Manages the communication layer object and registers itself with it as a callback to react to changes on the communication layer. """ def __init__(self, fileManager, analysisQueue, printerProfileManager): from collections import deque self._logger = logging.getLogger(__name__) self._analysisQueue = analysisQueue self._fileManager = fileManager self._printerProfileManager = printerProfileManager # state # TODO do we really need to hold the temperature here? self._temp = None self._bedTemp = None self._targetTemp = None self._targetBedTemp = None self._temps = TemperatureHistory( cutoff=settings().getInt(["temperature", "cutoff"]) * 60) self._tempBacklog = [] self._latestMessage = None self._messages = deque([], 300) self._messageBacklog = [] self._latestLog = None self._log = deque([], 300) self._logBacklog = [] self._state = None self._currentZ = None self._progress = None self._printTime = None self._printTimeLeft = None self._printAfterSelect = False self._posAfterSelect = None # sd handling self._sdPrinting = False self._sdStreaming = False self._sdFilelistAvailable = threading.Event() self._streamingFinishedCallback = None self._selectedFile = None self._timeEstimationData = None # comm self._comm = None # callbacks self._callbacks = [] # progress plugins self._lastProgressReport = None self._progressPlugins = plugin_manager().get_implementations( ProgressPlugin) self._stateMonitor = StateMonitor( interval=0.5, on_update=self._sendCurrentDataCallbacks, on_add_temperature=self._sendAddTemperatureCallbacks, on_add_log=self._sendAddLogCallbacks, on_add_message=self._sendAddMessageCallbacks) self._stateMonitor.reset(state={ "text": self.get_state_string(), "flags": self._getStateFlags() }, job_data={ "file": { "name": None, "size": None, "origin": None, "date": None }, "estimatedPrintTime": None, "lastPrintTime": None, "filament": { "length": None, "volume": None } }, progress={ "completion": None, "filepos": None, "printTime": None, "printTimeLeft": None }, current_z=None) eventManager().subscribe(Events.METADATA_ANALYSIS_FINISHED, self._on_event_MetadataAnalysisFinished) eventManager().subscribe(Events.METADATA_STATISTICS_UPDATED, self._on_event_MetadataStatisticsUpdated) #~~ handling of PrinterCallbacks def register_callback(self, callback): if not isinstance(callback, PrinterCallback): self._logger.warn( "Registering an object as printer callback which doesn't implement the PrinterCallback interface" ) self._callbacks.append(callback) self._sendInitialStateUpdate(callback) def unregister_callback(self, callback): if callback in self._callbacks: self._callbacks.remove(callback) def _sendAddTemperatureCallbacks(self, data): for callback in self._callbacks: try: callback.on_printer_add_temperature(data) except: self._logger.exception( "Exception while adding temperature data point") def _sendAddLogCallbacks(self, data): for callback in self._callbacks: try: callback.on_printer_add_log(data) except: self._logger.exception( "Exception while adding communication log entry") def _sendAddMessageCallbacks(self, data): for callback in self._callbacks: try: callback.on_printer_add_message(data) except: self._logger.exception( "Exception while adding printer message") def _sendCurrentDataCallbacks(self, data): for callback in self._callbacks: try: callback.on_printer_send_current_data(copy.deepcopy(data)) except: self._logger.exception("Exception while pushing current data") #~~ callback from metadata analysis event def _on_event_MetadataAnalysisFinished(self, event, data): if self._selectedFile: self._setJobData(self._selectedFile["filename"], self._selectedFile["filesize"], self._selectedFile["sd"]) def _on_event_MetadataStatisticsUpdated(self, event, data): self._setJobData(self._selectedFile["filename"], self._selectedFile["filesize"], self._selectedFile["sd"]) #~~ progress plugin reporting def _reportPrintProgressToPlugins(self, progress): if not progress or not self._selectedFile or not "sd" in self._selectedFile or not "filename" in self._selectedFile: return storage = "sdcard" if self._selectedFile["sd"] else "local" filename = self._selectedFile["filename"] def call_plugins(storage, filename, progress): for plugin in self._progressPlugins: try: plugin.on_print_progress(storage, filename, progress) except: self._logger.exception( "Exception while sending print progress to plugin %s" % plugin._identifier) thread = threading.Thread(target=call_plugins, args=(storage, filename, progress)) thread.daemon = False thread.start() #~~ PrinterInterface implementation def connect(self, port=None, baudrate=None, profile=None): """ Connects to the printer. If port and/or baudrate is provided, uses these settings, otherwise autodetection will be attempted. """ if self._comm is not None: self._comm.close() self._printerProfileManager.select(profile) self._comm = comm.MachineCom( port, baudrate, callbackObject=self, printerProfileManager=self._printerProfileManager) def disconnect(self): """ Closes the connection to the printer. """ if self._comm is not None: self._comm.close() self._comm = None self._printerProfileManager.deselect() eventManager().fire(Events.DISCONNECTED) def get_transport(self): if self._comm is None: return None return self._comm.getTransport() getTransport = util.deprecated( "getTransport has been renamed to get_transport", since="1.2.0-dev-590", includedoc="Replaced by :func:`get_transport`") def fake_ack(self): if self._comm is None: return self._comm.fakeOk() def commands(self, commands): """ Sends one or more gcode commands to the printer. """ if self._comm is None: return if not isinstance(commands, (list, tuple)): commands = [commands] for command in commands: self._comm.sendCommand(command) def script(self, name, context=None): if self._comm is None: return if name is None or not name: raise ValueError("name must be set") result = self._comm.sendGcodeScript(name, replacements=context) if not result: raise UnknownScript(name) def jog(self, axis, amount): if not isinstance(axis, (str, unicode)): raise ValueError("axis must be a string: {axis}".format(axis=axis)) axis = axis.lower() if not axis in PrinterInterface.valid_axes: raise ValueError("axis must be any of {axes}: {axis}".format( axes=", ".join(PrinterInterface.valid_axes), axis=axis)) if not isinstance(amount, (int, long, float)): raise ValueError("amount must be a valid number: {amount}".format( amount=amount)) printer_profile = self._printerProfileManager.get_current_or_default() movement_speed = printer_profile["axes"][axis]["speed"] self.commands([ "G91", "G1 %s%.4f F%d" % (axis.upper(), amount, movement_speed), "G90" ]) def home(self, axes): if not isinstance(axes, (list, tuple)): if isinstance(axes, (str, unicode)): axes = [axes] else: raise ValueError( "axes is neither a list nor a string: {axes}".format( axes=axes)) validated_axes = filter(lambda x: x in PrinterInterface.valid_axes, map(lambda x: x.lower(), axes)) if len(axes) != len(validated_axes): raise ValueError( "axes contains invalid axes: {axes}".format(axes=axes)) self.commands([ "G91", "G28 %s" % " ".join(map(lambda x: "%s0" % x.upper(), validated_axes)), "G90" ]) def extrude(self, amount): if not isinstance(amount, (int, long, float)): raise ValueError("amount must be a valid number: {amount}".format( amount=amount)) printer_profile = self._printerProfileManager.get_current_or_default() extrusion_speed = printer_profile["axes"]["e"]["speed"] self.commands(["G91", "G1 E%s F%d" % (amount, extrusion_speed), "G90"]) def change_tool(self, tool): if not PrinterInterface.valid_tool_regex.match(tool): raise ValueError( "tool must match \"tool[0-9]+\": {tool}".format(tool=tool)) tool_num = int(tool[len("tool"):]) self.commands("T%d" % tool_num) def set_temperature(self, heater, value): if not PrinterInterface.valid_heater_regex.match(heater): raise ValueError( "heater must match \"tool[0-9]+\" or \"bed\": {heater}".format( type=heater)) if not isinstance(value, (int, long, float)) or value < 0: raise ValueError( "value must be a valid number >= 0: {value}".format( value=value)) if heater.startswith("tool"): printer_profile = self._printerProfileManager.get_current_or_default( ) extruder_count = printer_profile["extruder"]["count"] if extruder_count > 1: toolNum = int(heater[len("tool"):]) self.commands("M104 T%d S%f" % (toolNum, value)) else: self.commands("M104 S%f" % value) elif heater == "bed": self.commands("M140 S%f" % value) def set_temperature_offset(self, offsets=None): if offsets is None: offsets = dict() if not isinstance(offsets, dict): raise ValueError("offsets must be a dict") validated_keys = filter( lambda x: PrinterInterface.valid_heater_regex.match(x), offsets.keys()) validated_values = filter(lambda x: isinstance(x, (int, long, float)), offsets.values()) if len(validated_keys) != len(offsets): raise ValueError("offsets contains invalid keys: {offsets}".format( offsets=offsets)) if len(validated_values) != len(offsets): raise ValueError( "offsets contains invalid values: {offsets}".format( offsets=offsets)) if self._comm is None: return self._comm.setTemperatureOffset(offsets) self._stateMonitor.set_temp_offsets(offsets) def _convert_rate_value(self, factor, min=0, max=200): if not isinstance(factor, (int, float, long)): raise ValueError("factor is not a number") if isinstance(factor, float): factor = int(factor * 100.0) if factor < min or factor > max: raise ValueError("factor must be a value between %f and %f" % (min, max)) return factor def feed_rate(self, factor): factor = self._convert_rate_value(factor, min=50, max=200) self.commands("M220 S%d" % factor) def flow_rate(self, factor): factor = self._convert_rate_value(factor, min=75, max=125) self.commands("M221 S%d" % factor) def select_file(self, path, sd, printAfterSelect=False, pos=None): if self._comm is None or (self._comm.isBusy() or self._comm.isStreaming()): self._logger.info( "Cannot load file: printer not connected or currently busy") return recovery_data = self._fileManager.get_recovery_data() if recovery_data: # clean up recovery data if we just selected a different file than is logged in that expected_origin = FileDestinations.SDCARD if sd else FileDestinations.LOCAL actual_origin = recovery_data.get("origin", None) actual_path = recovery_data.get("path", None) if actual_origin is None or actual_path is None or actual_origin != expected_origin or actual_path != path: self._fileManager.delete_recovery_data() self._printAfterSelect = printAfterSelect self._posAfterSelect = pos self._comm.selectFile("/" + path if sd else path, sd) self._setProgressData(0, None, None, None) self._setCurrentZ(None) def unselect_file(self): if self._comm is not None and (self._comm.isBusy() or self._comm.isStreaming()): return self._comm.unselectFile() self._setProgressData(0, None, None, None) self._setCurrentZ(None) def start_print(self, pos=None): """ Starts the currently loaded print job. Only starts if the printer is connected and operational, not currently printing and a printjob is loaded """ if self._comm is None or not self._comm.isOperational( ) or self._comm.isPrinting(): return if self._selectedFile is None: return rolling_window = None threshold = None countdown = None if self._selectedFile["sd"]: # we are interesting in a rolling window of roughly the last 15s, so the number of entries has to be derived # by that divided by the sd status polling interval rolling_window = 15 / settings().get( ["serial", "timeout", "sdStatus"]) # we are happy if the average of the estimates stays within 60s of the prior one threshold = 60 # we are happy when one rolling window has been stable countdown = rolling_window self._timeEstimationData = TimeEstimationHelper( rolling_window=rolling_window, threshold=threshold, countdown=countdown) self._fileManager.delete_recovery_data() self._lastProgressReport = None self._setProgressData(0, None, None, None) self._setCurrentZ(None) self._comm.startPrint(pos=pos) def toggle_pause_print(self): """ Pause the current printjob. """ if self._comm is None: return self._comm.setPause(not self._comm.isPaused()) def cancel_print(self): """ Cancel the current printjob. """ if self._comm is None: return self._comm.cancelPrint() # reset progress, height, print time self._setCurrentZ(None) self._setProgressData(None, None, None, None) # mark print as failure if self._selectedFile is not None: self._fileManager.log_print( FileDestinations.SDCARD if self._selectedFile["sd"] else FileDestinations.LOCAL, self._selectedFile["filename"], time.time(), self._comm.getPrintTime(), False, self._printerProfileManager.get_current_or_default()["id"]) payload = { "file": self._selectedFile["filename"], "origin": FileDestinations.LOCAL } if self._selectedFile["sd"]: payload["origin"] = FileDestinations.SDCARD eventManager().fire(Events.PRINT_FAILED, payload) def get_state_string(self): """ Returns a human readable string corresponding to the current communication state. """ if self._comm is None: return "Offline" else: return self._comm.getStateString() def get_current_data(self): return self._stateMonitor.get_current_data() def get_current_job(self): currentData = self._stateMonitor.get_current_data() return currentData["job"] def get_current_temperatures(self): if self._comm is not None: offsets = self._comm.getOffsets() else: offsets = dict() result = {} if self._temp is not None: for tool in self._temp.keys(): result["tool%d" % tool] = { "actual": self._temp[tool][0], "target": self._temp[tool][1], "offset": offsets[tool] if tool in offsets and offsets[tool] is not None else 0 } if self._bedTemp is not None: result["bed"] = { "actual": self._bedTemp[0], "target": self._bedTemp[1], "offset": offsets["bed"] if "bed" in offsets and offsets["bed"] is not None else 0 } return result def get_temperature_history(self): return self._temps def get_current_connection(self): if self._comm is None: return "Closed", None, None, None port, baudrate = self._comm.getConnection() printer_profile = self._printerProfileManager.get_current_or_default() return self._comm.getStateString(), port, baudrate, printer_profile def is_closed_or_error(self): return self._comm is None or self._comm.isClosedOrError() def is_operational(self): return self._comm is not None and self._comm.isOperational() def is_printing(self): return self._comm is not None and self._comm.isPrinting() def is_paused(self): return self._comm is not None and self._comm.isPaused() def is_error(self): return self._comm is not None and self._comm.isError() def is_ready(self): return self.is_operational() and not self._comm.isStreaming() def is_sd_ready(self): if not settings().getBoolean(["feature", "sdSupport" ]) or self._comm is None: return False else: return self._comm.isSdReady() #~~ sd file handling def get_sd_files(self): if self._comm is None or not self._comm.isSdReady(): return [] return map(lambda x: (x[0][1:], x[1]), self._comm.getSdFiles()) def add_sd_file(self, filename, absolutePath, streamingFinishedCallback): if not self._comm or self._comm.isBusy() or not self._comm.isSdReady(): self._logger.error("No connection to printer or printer is busy") return self._streamingFinishedCallback = streamingFinishedCallback self.refresh_sd_files(blocking=True) existingSdFiles = map(lambda x: x[0], self._comm.getSdFiles()) remoteName = util.get_dos_filename(filename, existing_filenames=existingSdFiles, extension="gco") self._timeEstimationData = TimeEstimationHelper() self._comm.startFileTransfer(absolutePath, filename, "/" + remoteName) return remoteName def delete_sd_file(self, filename): if not self._comm or not self._comm.isSdReady(): return self._comm.deleteSdFile("/" + filename) def init_sd_card(self): if not self._comm or self._comm.isSdReady(): return self._comm.initSdCard() def release_sd_card(self): if not self._comm or not self._comm.isSdReady(): return self._comm.releaseSdCard() def refresh_sd_files(self, blocking=False): """ Refreshs the list of file stored on the SD card attached to printer (if available and printer communication available). Optional blocking parameter allows making the method block (max 10s) until the file list has been received (and can be accessed via self._comm.getSdFiles()). Defaults to an asynchronous operation. """ if not self._comm or not self._comm.isSdReady(): return self._sdFilelistAvailable.clear() self._comm.refreshSdFiles() if blocking: self._sdFilelistAvailable.wait(10000) #~~ state monitoring def _setCurrentZ(self, currentZ): self._currentZ = currentZ self._stateMonitor.set_current_z(self._currentZ) def _setState(self, state): self._state = state self._stateMonitor.set_state({ "text": self.get_state_string(), "flags": self._getStateFlags() }) def _addLog(self, log): self._log.append(log) self._stateMonitor.add_log(log) def _addMessage(self, message): self._messages.append(message) self._stateMonitor.add_message(message) def _estimateTotalPrintTime(self, progress, printTime): if not progress or not printTime or not self._timeEstimationData: return None else: newEstimate = printTime / progress self._timeEstimationData.update(newEstimate) result = None if self._timeEstimationData.is_stable(): result = self._timeEstimationData.average_total_rolling return result def _setProgressData(self, progress, filepos, printTime, cleanedPrintTime): estimatedTotalPrintTime = self._estimateTotalPrintTime( progress, cleanedPrintTime) totalPrintTime = estimatedTotalPrintTime if self._selectedFile and "estimatedPrintTime" in self._selectedFile and self._selectedFile[ "estimatedPrintTime"]: statisticalTotalPrintTime = self._selectedFile[ "estimatedPrintTime"] if progress and cleanedPrintTime: if estimatedTotalPrintTime is None: totalPrintTime = statisticalTotalPrintTime else: if progress < 0.5: sub_progress = progress * 2 else: sub_progress = 1.0 totalPrintTime = ( 1 - sub_progress ) * statisticalTotalPrintTime + sub_progress * estimatedTotalPrintTime self._progress = progress self._printTime = printTime self._printTimeLeft = totalPrintTime - cleanedPrintTime if ( totalPrintTime is not None and cleanedPrintTime is not None) else None self._stateMonitor.set_progress({ "completion": self._progress * 100 if self._progress is not None else None, "filepos": filepos, "printTime": int(self._printTime) if self._printTime is not None else None, "printTimeLeft": int(self._printTimeLeft) if self._printTimeLeft is not None else None }) if progress: progress_int = int(progress * 100) if self._lastProgressReport != progress_int: self._lastProgressReport = progress_int self._reportPrintProgressToPlugins(progress_int) def _addTemperatureData(self, temp, bedTemp): currentTimeUtc = int(time.time()) data = {"time": currentTimeUtc} for tool in temp.keys(): data["tool%d" % tool] = { "actual": temp[tool][0], "target": temp[tool][1] } if bedTemp is not None and isinstance(bedTemp, tuple): data["bed"] = {"actual": bedTemp[0], "target": bedTemp[1]} self._temps.append(data) self._temp = temp self._bedTemp = bedTemp self._stateMonitor.add_temperature(data) def _setJobData(self, filename, filesize, sd): if filename is not None: if sd: path_in_storage = filename if path_in_storage.startswith("/"): path_in_storage = path_in_storage[1:] path_on_disk = None else: path_in_storage = self._fileManager.path_in_storage( FileDestinations.LOCAL, filename) path_on_disk = self._fileManager.path_on_disk( FileDestinations.LOCAL, filename) self._selectedFile = { "filename": path_in_storage, "filesize": filesize, "sd": sd, "estimatedPrintTime": None } else: self._selectedFile = None self._stateMonitor.set_job_data({ "file": { "name": None, "origin": None, "size": None, "date": None }, "estimatedPrintTime": None, "averagePrintTime": None, "lastPrintTime": None, "filament": None, }) return estimatedPrintTime = None lastPrintTime = None averagePrintTime = None date = None filament = None if path_on_disk: # Use a string for mtime because it could be float and the # javascript needs to exact match if not sd: date = int(os.stat(path_on_disk).st_mtime) try: fileData = self._fileManager.get_metadata( FileDestinations.SDCARD if sd else FileDestinations.LOCAL, path_on_disk) except: fileData = None if fileData is not None: if "analysis" in fileData: if estimatedPrintTime is None and "estimatedPrintTime" in fileData[ "analysis"]: estimatedPrintTime = fileData["analysis"][ "estimatedPrintTime"] if "filament" in fileData["analysis"].keys(): filament = fileData["analysis"]["filament"] if "statistics" in fileData: printer_profile = self._printerProfileManager.get_current_or_default( )["id"] if "averagePrintTime" in fileData[ "statistics"] and printer_profile in fileData[ "statistics"]["averagePrintTime"]: averagePrintTime = fileData["statistics"][ "averagePrintTime"][printer_profile] if "lastPrintTime" in fileData[ "statistics"] and printer_profile in fileData[ "statistics"]["lastPrintTime"]: lastPrintTime = fileData["statistics"][ "lastPrintTime"][printer_profile] if averagePrintTime is not None: self._selectedFile["estimatedPrintTime"] = averagePrintTime elif estimatedPrintTime is not None: # TODO apply factor which first needs to be tracked! self._selectedFile[ "estimatedPrintTime"] = estimatedPrintTime self._stateMonitor.set_job_data({ "file": { "name": path_in_storage, "origin": FileDestinations.SDCARD if sd else FileDestinations.LOCAL, "size": filesize, "date": date }, "estimatedPrintTime": estimatedPrintTime, "averagePrintTime": averagePrintTime, "lastPrintTime": lastPrintTime, "filament": filament, }) def _sendInitialStateUpdate(self, callback): try: data = self._stateMonitor.get_current_data() data.update({ "temps": list(self._temps), "logs": list(self._log), "messages": list(self._messages) }) callback.on_printer_send_initial_data(data) except Exception, err: import sys sys.stderr.write("ERROR: %s\n" % str(err)) pass
class Printer(PrinterInterface, comm.MachineComPrintCallback): """ Default implementation of the :class:`PrinterInterface`. Manages the communication layer object and registers itself with it as a callback to react to changes on the communication layer. """ def __init__(self, fileManager, analysisQueue, printerProfileManager): from collections import deque self._logger = logging.getLogger(__name__) self._analysisQueue = analysisQueue self._fileManager = fileManager self._printerProfileManager = printerProfileManager # state # TODO do we really need to hold the temperature here? self._temp = None self._bedTemp = None self._targetTemp = None self._targetBedTemp = None self._temps = TemperatureHistory(cutoff=settings().getInt(["temperature", "cutoff"])*60) self._tempBacklog = [] self._messages = deque([], 300) self._messageBacklog = [] self._log = deque([], 300) self._logBacklog = [] self._state = None self._currentZ = None self._printAfterSelect = False self._posAfterSelect = None # sd handling self._sdPrinting = False self._sdStreaming = False self._sdFilelistAvailable = threading.Event() self._streamingFinishedCallback = None self._selectedFile = None self._timeEstimationData = None self._timeEstimationStatsWeighingUntil = settings().getFloat(["estimation", "printTime", "statsWeighingUntil"]) self._timeEstimationValidityRange = settings().getFloat(["estimation", "printTime", "validityRange"]) self._timeEstimationForceDumbFromPercent = settings().getFloat(["estimation", "printTime", "forceDumbFromPercent"]) self._timeEstimationForceDumbAfterMin = settings().getFloat(["estimation", "printTime", "forceDumbAfterMin"]) # comm self._comm = None # callbacks self._callbacks = [] # progress plugins self._lastProgressReport = None self._progressPlugins = plugin_manager().get_implementations(ProgressPlugin) self._stateMonitor = StateMonitor( interval=0.5, on_update=self._sendCurrentDataCallbacks, on_add_temperature=self._sendAddTemperatureCallbacks, on_add_log=self._sendAddLogCallbacks, on_add_message=self._sendAddMessageCallbacks, on_get_progress=self._updateProgressDataCallback ) self._stateMonitor.reset( state={"text": self.get_state_string(), "flags": self._getStateFlags()}, job_data={ "file": { "name": None, "size": None, "origin": None, "date": None }, "estimatedPrintTime": None, "lastPrintTime": None, "filament": { "length": None, "volume": None } }, progress={"completion": None, "filepos": None, "printTime": None, "printTimeLeft": None}, current_z=None ) eventManager().subscribe(Events.METADATA_ANALYSIS_FINISHED, self._on_event_MetadataAnalysisFinished) eventManager().subscribe(Events.METADATA_STATISTICS_UPDATED, self._on_event_MetadataStatisticsUpdated) #~~ handling of PrinterCallbacks def register_callback(self, callback): if not isinstance(callback, PrinterCallback): self._logger.warn("Registering an object as printer callback which doesn't implement the PrinterCallback interface") self._callbacks.append(callback) self._sendInitialStateUpdate(callback) def unregister_callback(self, callback): if callback in self._callbacks: self._callbacks.remove(callback) def _sendAddTemperatureCallbacks(self, data): for callback in self._callbacks: try: callback.on_printer_add_temperature(data) except: self._logger.exception("Exception while adding temperature data point") def _sendAddLogCallbacks(self, data): for callback in self._callbacks: try: callback.on_printer_add_log(data) except: self._logger.exception("Exception while adding communication log entry") def _sendAddMessageCallbacks(self, data): for callback in self._callbacks: try: callback.on_printer_add_message(data) except: self._logger.exception("Exception while adding printer message") def _sendCurrentDataCallbacks(self, data): for callback in self._callbacks: try: callback.on_printer_send_current_data(copy.deepcopy(data)) except: self._logger.exception("Exception while pushing current data") #~~ callback from metadata analysis event def _on_event_MetadataAnalysisFinished(self, event, data): if self._selectedFile: self._setJobData(self._selectedFile["filename"], self._selectedFile["filesize"], self._selectedFile["sd"]) def _on_event_MetadataStatisticsUpdated(self, event, data): self._setJobData(self._selectedFile["filename"], self._selectedFile["filesize"], self._selectedFile["sd"]) #~~ progress plugin reporting def _reportPrintProgressToPlugins(self, progress): if not progress or not self._selectedFile or not "sd" in self._selectedFile or not "filename" in self._selectedFile: return storage = "sdcard" if self._selectedFile["sd"] else "local" filename = self._selectedFile["filename"] def call_plugins(storage, filename, progress): for plugin in self._progressPlugins: try: plugin.on_print_progress(storage, filename, progress) except: self._logger.exception("Exception while sending print progress to plugin %s" % plugin._identifier) thread = threading.Thread(target=call_plugins, args=(storage, filename, progress)) thread.daemon = False thread.start() #~~ PrinterInterface implementation def connect(self, port=None, baudrate=None, profile=None): """ Connects to the printer. If port and/or baudrate is provided, uses these settings, otherwise autodetection will be attempted. """ if self._comm is not None: self._comm.close() self._printerProfileManager.select(profile) self._comm = comm.MachineCom(port, baudrate, callbackObject=self, printerProfileManager=self._printerProfileManager) def disconnect(self): """ Closes the connection to the printer. """ if self._comm is not None: self._comm.close() self._comm = None self._printerProfileManager.deselect() eventManager().fire(Events.DISCONNECTED) def get_transport(self): if self._comm is None: return None return self._comm.getTransport() getTransport = util.deprecated("getTransport has been renamed to get_transport", since="1.2.0-dev-590", includedoc="Replaced by :func:`get_transport`") def fake_ack(self): if self._comm is None: return self._comm.fakeOk() def commands(self, commands): """ Sends one or more gcode commands to the printer. """ if self._comm is None: return if not isinstance(commands, (list, tuple)): commands = [commands] for command in commands: self._comm.sendCommand(command) def script(self, name, context=None): if self._comm is None: return if name is None or not name: raise ValueError("name must be set") result = self._comm.sendGcodeScript(name, replacements=context) if not result: raise UnknownScript(name) def jog(self, axis, amount): if not isinstance(axis, (str, unicode)): raise ValueError("axis must be a string: {axis}".format(axis=axis)) axis = axis.lower() if not axis in PrinterInterface.valid_axes: raise ValueError("axis must be any of {axes}: {axis}".format(axes=", ".join(PrinterInterface.valid_axes), axis=axis)) if not isinstance(amount, (int, long, float)): raise ValueError("amount must be a valid number: {amount}".format(amount=amount)) printer_profile = self._printerProfileManager.get_current_or_default() movement_speed = printer_profile["axes"][axis]["speed"] self.commands(["G91", "G1 %s%.4f F%d" % (axis.upper(), amount, movement_speed), "G90"]) def home(self, axes): if not isinstance(axes, (list, tuple)): if isinstance(axes, (str, unicode)): axes = [axes] else: raise ValueError("axes is neither a list nor a string: {axes}".format(axes=axes)) validated_axes = filter(lambda x: x in PrinterInterface.valid_axes, map(lambda x: x.lower(), axes)) if len(axes) != len(validated_axes): raise ValueError("axes contains invalid axes: {axes}".format(axes=axes)) self.commands(["G91", "G28 %s" % " ".join(map(lambda x: "%s0" % x.upper(), validated_axes)), "G90"]) def extrude(self, amount): if not isinstance(amount, (int, long, float)): raise ValueError("amount must be a valid number: {amount}".format(amount=amount)) printer_profile = self._printerProfileManager.get_current_or_default() extrusion_speed = printer_profile["axes"]["e"]["speed"] self.commands(["G91", "G1 E%s F%d" % (amount, extrusion_speed), "G90"]) def change_tool(self, tool): if not PrinterInterface.valid_tool_regex.match(tool): raise ValueError("tool must match \"tool[0-9]+\": {tool}".format(tool=tool)) tool_num = int(tool[len("tool"):]) self.commands("T%d" % tool_num) def set_temperature(self, heater, value): if not PrinterInterface.valid_heater_regex.match(heater): raise ValueError("heater must match \"tool[0-9]+\" or \"bed\": {heater}".format(type=heater)) if not isinstance(value, (int, long, float)) or value < 0: raise ValueError("value must be a valid number >= 0: {value}".format(value=value)) if heater.startswith("tool"): printer_profile = self._printerProfileManager.get_current_or_default() extruder_count = printer_profile["extruder"]["count"] if extruder_count > 1: toolNum = int(heater[len("tool"):]) self.commands("M104 T%d S%f" % (toolNum, value)) else: self.commands("M104 S%f" % value) elif heater == "bed": self.commands("M140 S%f" % value) def set_temperature_offset(self, offsets=None): if offsets is None: offsets = dict() if not isinstance(offsets, dict): raise ValueError("offsets must be a dict") validated_keys = filter(lambda x: PrinterInterface.valid_heater_regex.match(x), offsets.keys()) validated_values = filter(lambda x: isinstance(x, (int, long, float)), offsets.values()) if len(validated_keys) != len(offsets): raise ValueError("offsets contains invalid keys: {offsets}".format(offsets=offsets)) if len(validated_values) != len(offsets): raise ValueError("offsets contains invalid values: {offsets}".format(offsets=offsets)) if self._comm is None: return self._comm.setTemperatureOffset(offsets) self._stateMonitor.set_temp_offsets(offsets) def _convert_rate_value(self, factor, min=0, max=200): if not isinstance(factor, (int, float, long)): raise ValueError("factor is not a number") if isinstance(factor, float): factor = int(factor * 100.0) if factor < min or factor > max: raise ValueError("factor must be a value between %f and %f" % (min, max)) return factor def feed_rate(self, factor): factor = self._convert_rate_value(factor, min=50, max=200) self.commands("M220 S%d" % factor) def flow_rate(self, factor): factor = self._convert_rate_value(factor, min=75, max=125) self.commands("M221 S%d" % factor) def select_file(self, path, sd, printAfterSelect=False, pos=None): if self._comm is None or (self._comm.isBusy() or self._comm.isStreaming()): self._logger.info("Cannot load file: printer not connected or currently busy") return recovery_data = self._fileManager.get_recovery_data() if recovery_data: # clean up recovery data if we just selected a different file than is logged in that expected_origin = FileDestinations.SDCARD if sd else FileDestinations.LOCAL actual_origin = recovery_data.get("origin", None) actual_path = recovery_data.get("path", None) if actual_origin is None or actual_path is None or actual_origin != expected_origin or actual_path != path: self._fileManager.delete_recovery_data() self._printAfterSelect = printAfterSelect self._posAfterSelect = pos self._comm.selectFile("/" + path if sd and not settings().getBoolean(["feature", "sdRelativePath"]) else path, sd) self._setProgressData(completion=0) self._setCurrentZ(None) def unselect_file(self): if self._comm is not None and (self._comm.isBusy() or self._comm.isStreaming()): return self._comm.unselectFile() self._setProgressData(completion=0) self._setCurrentZ(None) def start_print(self, pos=None): """ Starts the currently loaded print job. Only starts if the printer is connected and operational, not currently printing and a printjob is loaded """ if self._comm is None or not self._comm.isOperational() or self._comm.isPrinting(): return if self._selectedFile is None: return # we are happy if the average of the estimates stays within 60s of the prior one threshold = settings().getFloat(["estimation", "printTime", "stableThreshold"]) rolling_window = None countdown = None if self._selectedFile["sd"]: # we are interesting in a rolling window of roughly the last 15s, so the number of entries has to be derived # by that divided by the sd status polling interval rolling_window = 15 / settings().get(["serial", "timeout", "sdStatus"]) # we are happy when one rolling window has been stable countdown = rolling_window self._timeEstimationData = TimeEstimationHelper(rolling_window=rolling_window, threshold=threshold, countdown=countdown) self._fileManager.delete_recovery_data() self._lastProgressReport = None self._setProgressData(completion=0) self._setCurrentZ(None) self._comm.startPrint(pos=pos) def pause_print(self): """ Pause the current printjob. """ if self._comm is None: return if self._comm.isPaused(): return self._comm.setPause(True) def resume_print(self): """ Resume the current printjob. """ if self._comm is None: return if not self._comm.isPaused(): return self._comm.setPause(False) def cancel_print(self): """ Cancel the current printjob. """ if self._comm is None: return self._comm.cancelPrint() # reset progress, height, print time self._setCurrentZ(None) self._setProgressData() # mark print as failure if self._selectedFile is not None: self._fileManager.log_print(FileDestinations.SDCARD if self._selectedFile["sd"] else FileDestinations.LOCAL, self._selectedFile["filename"], time.time(), self._comm.getPrintTime(), False, self._printerProfileManager.get_current_or_default()["id"]) payload = { "file": self._selectedFile["filename"], "origin": FileDestinations.LOCAL } if self._selectedFile["sd"]: payload["origin"] = FileDestinations.SDCARD eventManager().fire(Events.PRINT_FAILED, payload) def get_state_string(self): """ Returns a human readable string corresponding to the current communication state. """ if self._comm is None: return "Offline" else: return self._comm.getStateString() def get_current_data(self): return self._stateMonitor.get_current_data() def get_current_job(self): currentData = self._stateMonitor.get_current_data() return currentData["job"] def get_current_temperatures(self): if self._comm is not None: offsets = self._comm.getOffsets() else: offsets = dict() result = {} if self._temp is not None: for tool in self._temp.keys(): result["tool%d" % tool] = { "actual": self._temp[tool][0], "target": self._temp[tool][1], "offset": offsets[tool] if tool in offsets and offsets[tool] is not None else 0 } if self._bedTemp is not None: result["bed"] = { "actual": self._bedTemp[0], "target": self._bedTemp[1], "offset": offsets["bed"] if "bed" in offsets and offsets["bed"] is not None else 0 } return result def get_temperature_history(self): return self._temps def get_current_connection(self): if self._comm is None: return "Closed", None, None, None port, baudrate = self._comm.getConnection() printer_profile = self._printerProfileManager.get_current_or_default() return self._comm.getStateString(), port, baudrate, printer_profile def is_closed_or_error(self): return self._comm is None or self._comm.isClosedOrError() def is_operational(self): return self._comm is not None and self._comm.isOperational() def is_printing(self): return self._comm is not None and self._comm.isPrinting() def is_paused(self): return self._comm is not None and self._comm.isPaused() def is_error(self): return self._comm is not None and self._comm.isError() def is_ready(self): return self.is_operational() and not self._comm.isStreaming() def is_sd_ready(self): if not settings().getBoolean(["feature", "sdSupport"]) or self._comm is None: return False else: return self._comm.isSdReady() #~~ sd file handling def get_sd_files(self): if self._comm is None or not self._comm.isSdReady(): return [] return map(lambda x: (x[0][1:], x[1]), self._comm.getSdFiles()) def add_sd_file(self, filename, absolutePath, streamingFinishedCallback): if not self._comm or self._comm.isBusy() or not self._comm.isSdReady(): self._logger.error("No connection to printer or printer is busy") return self._streamingFinishedCallback = streamingFinishedCallback self.refresh_sd_files(blocking=True) existingSdFiles = map(lambda x: x[0], self._comm.getSdFiles()) remoteName = util.get_dos_filename(filename, existing_filenames=existingSdFiles, extension="gco", whitelisted_extensions=["gco", "g"]) self._timeEstimationData = TimeEstimationHelper() self._comm.startFileTransfer(absolutePath, filename, "/" + remoteName) return remoteName def delete_sd_file(self, filename): if not self._comm or not self._comm.isSdReady(): return self._comm.deleteSdFile("/" + filename) def init_sd_card(self): if not self._comm or self._comm.isSdReady(): return self._comm.initSdCard() def release_sd_card(self): if not self._comm or not self._comm.isSdReady(): return self._comm.releaseSdCard() def refresh_sd_files(self, blocking=False): """ Refreshs the list of file stored on the SD card attached to printer (if available and printer communication available). Optional blocking parameter allows making the method block (max 10s) until the file list has been received (and can be accessed via self._comm.getSdFiles()). Defaults to an asynchronous operation. """ if not self._comm or not self._comm.isSdReady(): return self._sdFilelistAvailable.clear() self._comm.refreshSdFiles() if blocking: self._sdFilelistAvailable.wait(10000) #~~ state monitoring def _setCurrentZ(self, currentZ): self._currentZ = currentZ self._stateMonitor.set_current_z(self._currentZ) def _setState(self, state, state_string=None): if state_string is None: state_string = self.get_state_string() self._state = state self._stateMonitor.set_state({"text": state_string, "flags": self._getStateFlags()}) def _addLog(self, log): self._log.append(log) self._stateMonitor.add_log(log) def _addMessage(self, message): self._messages.append(message) self._stateMonitor.add_message(message) def _estimateTotalPrintTime(self, progress, printTime): if not progress or not printTime or not self._timeEstimationData: return None else: newEstimate = printTime / progress self._timeEstimationData.update(newEstimate) result = None if self._timeEstimationData.is_stable(): result = self._timeEstimationData.average_total_rolling return result def _setProgressData(self, completion=None, filepos=None, printTime=None, printTimeLeft=None): self._stateMonitor.set_progress(dict(completion=int(completion * 100) if completion is not None else None, filepos=filepos, printTime=int(printTime) if printTime is not None else None, printTimeLeft=int(printTimeLeft) if printTimeLeft is not None else None)) def _updateProgressDataCallback(self): if self._comm is None: progress = None filepos = None printTime = None cleanedPrintTime = None else: progress = self._comm.getPrintProgress() filepos = self._comm.getPrintFilepos() printTime = self._comm.getPrintTime() cleanedPrintTime = self._comm.getCleanedPrintTime() statisticalTotalPrintTime = None statisticalTotalPrintTimeType = None if self._selectedFile and "estimatedPrintTime" in self._selectedFile \ and self._selectedFile["estimatedPrintTime"]: statisticalTotalPrintTime = self._selectedFile["estimatedPrintTime"] statisticalTotalPrintTimeType = self._selectedFile.get("estimatedPrintTimeType", None) printTimeLeft, printTimeLeftOrigin = self._estimatePrintTimeLeft(progress, printTime, cleanedPrintTime, statisticalTotalPrintTime, statisticalTotalPrintTimeType) if progress is not None: progress_int = int(progress * 100) if self._lastProgressReport != progress_int: self._lastProgressReport = progress_int self._reportPrintProgressToPlugins(progress_int) return dict(completion=progress * 100 if progress is not None else None, filepos=filepos, printTime=int(printTime) if printTime is not None else None, printTimeLeft=int(printTimeLeft) if printTimeLeft is not None else None, printTimeLeftOrigin=printTimeLeftOrigin) def _estimatePrintTimeLeft(self, progress, printTime, cleanedPrintTime, statisticalTotalPrintTime, statisticalTotalPrintTimeType): """ Tries to estimate the print time left for the print job This is somewhat horrible since accurate print time estimation is pretty much impossible to achieve, considering that we basically have only two data points (current progress in file and time needed for that so far - former prints or a file analysis might not have happened or simply be completely impossible e.g. if the file is stored on the printer's SD card) and hence can only do a linear estimation of a completely non-linear process. That's a recipe for inaccurate predictions right there. Yay. Anyhow, here's how this implementation works. This method gets the current progress in the printed file (percentage based on bytes read vs total bytes), the print time that elapsed, the same print time with the heat up times subtracted (if possible) and if available also some statistical total print time (former prints or a result from the GCODE analysis). 1. First get an "intelligent" estimate based on the :class:`~octoprint.printer.estimation.TimeEstimationHelper`. That thing tries to detect if the estimation based on our progress and time needed for that becomes stable over time through a rolling window and only returns a result once that appears to be the case. 2. If we have any statistical data (former prints or a result from the GCODE analysis) but no intelligent estimate yet, we'll use that for the next step. Otherwise, up to a certain percentage in the print we do a percentage based weighing of the statistical data and the intelligent estimate - the closer to the beginning of the print, the more precedence for the statistical data, the closer to the cut off point, the more precendence for the intelligent estimate. This is our preliminary total print time. 3. If the total print time is set, we do a sanity check for it. Based on the total print time estimate and the time we already spent printing, we calculate at what percentage we SHOULD be and compare that to the percentage at which we actually ARE. If it's too far off, our total can't be trusted and we fall back on the dumb estimate. Same if the time we spent printing is already higher than our total estimate. 4. If we do NOT have a total print time estimate yet but we've been printing for longer than a configured amount of minutes or are further in the file than a configured percentage, we also use the dumb estimate for now. Yes, all this still produces horribly inaccurate results. But we have to do this live during the print and hence can't produce to much computational overhead, we do not have any insight into the firmware implementation with regards to planner setup and acceleration settings, we might not even have access to the printed file's contents and such we need to find something that works "mostly" all of the time without costing too many resources. Feel free to propose a better solution within the above limitations (and I mean that, this solution here makes me unhappy). Args: progress (float or None): Current percentage in the printed file printTime (float or None): Print time elapsed so far cleanedPrintTime (float or None): Print time elapsed minus the time needed for getting up to temperature (if detectable). statisticalTotalPrintTime (float or None): Total print time of past prints against same printer profile, or estimated total print time from GCODE analysis. statisticalTotalPrintTimeType (str or None): Type of statistical print time, either "average" (total time of former prints) or "analysis" Returns: (2-tuple) estimated print time left or None if not proper estimate could be made at all, origin of estimation """ if progress is None or printTime is None or cleanedPrintTime is None: return None dumbTotalPrintTime = printTime / progress estimatedTotalPrintTime = self._estimateTotalPrintTime(progress, cleanedPrintTime) totalPrintTime = estimatedTotalPrintTime printTimeLeftOrigin = "estimate" if statisticalTotalPrintTime is not None: if estimatedTotalPrintTime is None: # no estimate yet, we'll use the statistical total totalPrintTime = statisticalTotalPrintTime printTimeLeftOrigin = statisticalTotalPrintTimeType else: if progress < self._timeEstimationStatsWeighingUntil: # still inside weighing range, use part stats, part current estimate sub_progress = progress * (1 / self._timeEstimationStatsWeighingUntil) if sub_progress > 1.0: sub_progress = 1.0 printTimeLeftOrigin = "mixed-" + statisticalTotalPrintTimeType else: # use only the current estimate sub_progress = 1.0 printTimeLeftOrigin = "estimate" # combine totalPrintTime = (1.0 - sub_progress) * statisticalTotalPrintTime \ + sub_progress * estimatedTotalPrintTime printTimeLeft = None if totalPrintTime is not None: # sanity check current total print time estimate assumed_progress = cleanedPrintTime / totalPrintTime min_progress = progress - self._timeEstimationValidityRange max_progress = progress + self._timeEstimationValidityRange if min_progress <= assumed_progress <= max_progress and totalPrintTime > cleanedPrintTime: # appears sane, we'll use it printTimeLeft = totalPrintTime - cleanedPrintTime else: # too far from the actual progress or negative, # we use the dumb print time instead printTimeLeft = dumbTotalPrintTime - cleanedPrintTime printTimeLeftOrigin = "linear" else: printTimeLeftOrigin = "linear" if progress > self._timeEstimationForceDumbFromPercent or \ cleanedPrintTime >= self._timeEstimationForceDumbAfterMin * 60: # more than x% or y min printed and still no real estimate, ok, we'll use the dumb variant :/ printTimeLeft = dumbTotalPrintTime - cleanedPrintTime if printTimeLeft is not None and printTimeLeft < 0: # shouldn't actually happen, but let's make sure printTimeLeft = None return printTimeLeft, printTimeLeftOrigin def _addTemperatureData(self, temp, bedTemp): currentTimeUtc = int(time.time()) data = { "time": currentTimeUtc } for tool in temp.keys(): data["tool%d" % tool] = { "actual": temp[tool][0], "target": temp[tool][1] } if bedTemp is not None and isinstance(bedTemp, tuple): data["bed"] = { "actual": bedTemp[0], "target": bedTemp[1] } self._temps.append(data) self._temp = temp self._bedTemp = bedTemp self._stateMonitor.add_temperature(data) def _setJobData(self, filename, filesize, sd): if filename is not None: if sd: path_in_storage = filename if path_in_storage.startswith("/"): path_in_storage = path_in_storage[1:] path_on_disk = None else: path_in_storage = self._fileManager.path_in_storage(FileDestinations.LOCAL, filename) path_on_disk = self._fileManager.path_on_disk(FileDestinations.LOCAL, filename) self._selectedFile = { "filename": path_in_storage, "filesize": filesize, "sd": sd, "estimatedPrintTime": None } else: self._selectedFile = None self._stateMonitor.set_job_data({ "file": { "name": None, "origin": None, "size": None, "date": None }, "estimatedPrintTime": None, "averagePrintTime": None, "lastPrintTime": None, "filament": None, }) return estimatedPrintTime = None lastPrintTime = None averagePrintTime = None date = None filament = None if path_on_disk: # Use a string for mtime because it could be float and the # javascript needs to exact match if not sd: date = int(os.stat(path_on_disk).st_mtime) try: fileData = self._fileManager.get_metadata(FileDestinations.SDCARD if sd else FileDestinations.LOCAL, path_on_disk) except: fileData = None if fileData is not None: if "analysis" in fileData: if estimatedPrintTime is None and "estimatedPrintTime" in fileData["analysis"]: estimatedPrintTime = fileData["analysis"]["estimatedPrintTime"] if "filament" in fileData["analysis"].keys(): filament = fileData["analysis"]["filament"] if "statistics" in fileData: printer_profile = self._printerProfileManager.get_current_or_default()["id"] if "averagePrintTime" in fileData["statistics"] and printer_profile in fileData["statistics"]["averagePrintTime"]: averagePrintTime = fileData["statistics"]["averagePrintTime"][printer_profile] if "lastPrintTime" in fileData["statistics"] and printer_profile in fileData["statistics"]["lastPrintTime"]: lastPrintTime = fileData["statistics"]["lastPrintTime"][printer_profile] if averagePrintTime is not None: self._selectedFile["estimatedPrintTime"] = averagePrintTime self._selectedFile["estimatedPrintTimeType"] = "average" elif estimatedPrintTime is not None: # TODO apply factor which first needs to be tracked! self._selectedFile["estimatedPrintTime"] = estimatedPrintTime self._selectedFile["estimatedPrintTimeType"] = "analysis" self._stateMonitor.set_job_data({ "file": { "name": path_in_storage, "origin": FileDestinations.SDCARD if sd else FileDestinations.LOCAL, "size": filesize, "date": date }, "estimatedPrintTime": estimatedPrintTime, "averagePrintTime": averagePrintTime, "lastPrintTime": lastPrintTime, "filament": filament, }) def _sendInitialStateUpdate(self, callback): try: data = self._stateMonitor.get_current_data() data.update({ "temps": list(self._temps), "logs": list(self._log), "messages": list(self._messages) }) callback.on_printer_send_initial_data(data) except: self._logger.exception("Error while trying to send inital state update") def _getStateFlags(self): return { "operational": self.is_operational(), "printing": self.is_printing(), "closedOrError": self.is_closed_or_error(), "error": self.is_error(), "paused": self.is_paused(), "ready": self.is_ready(), "sdReady": self.is_sd_ready() } #~~ comm.MachineComPrintCallback implementation def on_comm_log(self, message): """ Callback method for the comm object, called upon log output. """ self._addLog(to_unicode(message, "utf-8", errors="replace")) def on_comm_temperature_update(self, temp, bedTemp): self._addTemperatureData(temp, bedTemp) def on_comm_state_change(self, state): """ Callback method for the comm object, called if the connection state changes. """ oldState = self._state state_string = None if self._comm is not None: state_string = self._comm.getStateString() # forward relevant state changes to gcode manager if oldState == comm.MachineCom.STATE_PRINTING: if self._selectedFile is not None: if state == comm.MachineCom.STATE_CLOSED or state == comm.MachineCom.STATE_ERROR or state == comm.MachineCom.STATE_CLOSED_WITH_ERROR: self._fileManager.log_print(FileDestinations.SDCARD if self._selectedFile["sd"] else FileDestinations.LOCAL, self._selectedFile["filename"], time.time(), self._comm.getPrintTime(), False, self._printerProfileManager.get_current_or_default()["id"]) self._analysisQueue.resume() # printing done, put those cpu cycles to good use elif state == comm.MachineCom.STATE_PRINTING: self._analysisQueue.pause() # do not analyse files while printing if state == comm.MachineCom.STATE_CLOSED or state == comm.MachineCom.STATE_CLOSED_WITH_ERROR: if self._comm is not None: self._comm = None self._setProgressData(completion=0) self._setCurrentZ(None) self._setJobData(None, None, None) self._setState(state, state_string=state_string) def on_comm_message(self, message): """ Callback method for the comm object, called upon message exchanges via serial. Stores the message in the message buffer, truncates buffer to the last 300 lines. """ self._addMessage(to_unicode(message, "utf-8", errors="replace")) def on_comm_progress(self): """ Callback method for the comm object, called upon any change in progress of the printjob. Triggers storage of new values for printTime, printTimeLeft and the current progress. """ self._stateMonitor.trigger_progress_update() def on_comm_z_change(self, newZ): """ Callback method for the comm object, called upon change of the z-layer. """ oldZ = self._currentZ if newZ != oldZ: # we have to react to all z-changes, even those that might "go backward" due to a slicer's retraction or # anti-backlash-routines. Event subscribes should individually take care to filter out "wrong" z-changes eventManager().fire(Events.Z_CHANGE, {"new": newZ, "old": oldZ}) self._setCurrentZ(newZ) def on_comm_sd_state_change(self, sdReady): self._stateMonitor.set_state({"text": self.get_state_string(), "flags": self._getStateFlags()}) def on_comm_sd_files(self, files): eventManager().fire(Events.UPDATED_FILES, {"type": "gcode"}) self._sdFilelistAvailable.set() def on_comm_file_selected(self, filename, filesize, sd): self._setJobData(filename, filesize, sd) self._stateMonitor.set_state({"text": self.get_state_string(), "flags": self._getStateFlags()}) if self._printAfterSelect: self._printAfterSelect = False self.start_print(pos=self._posAfterSelect) def on_comm_print_job_done(self): self._fileManager.log_print(FileDestinations.SDCARD if self._selectedFile["sd"] else FileDestinations.LOCAL, self._selectedFile["filename"], time.time(), self._comm.getPrintTime(), True, self._printerProfileManager.get_current_or_default()["id"]) self._setProgressData(completion=1.0, filepos=self._selectedFile["filesize"], printTime=self._comm.getPrintTime(), printTimeLeft=0) self._stateMonitor.set_state({"text": self.get_state_string(), "flags": self._getStateFlags()}) self._fileManager.delete_recovery_data() def on_comm_file_transfer_started(self, filename, filesize): self._sdStreaming = True self._setJobData(filename, filesize, True) self._setProgressData(completion=0.0, filepos=0, printTime=0) self._stateMonitor.set_state({"text": self.get_state_string(), "flags": self._getStateFlags()}) def on_comm_file_transfer_done(self, filename): self._sdStreaming = False if self._streamingFinishedCallback is not None: # in case of SD files, both filename and absolutePath are the same, so we set the (remote) filename for # both parameters self._streamingFinishedCallback(filename, filename, FileDestinations.SDCARD) self._setCurrentZ(None) self._setJobData(None, None, None) self._setProgressData() self._stateMonitor.set_state({"text": self.get_state_string(), "flags": self._getStateFlags()}) def on_comm_force_disconnect(self): self.disconnect() def on_comm_record_fileposition(self, origin, name, pos): try: self._fileManager.save_recovery_data(origin, name, pos) except NoSuchStorage: pass except: self._logger.exception("Error while trying to persist print recovery data")
__license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agpl.html" __copyright__ = "Copyright (C) 2022 The OctoPrint Project - Released under terms of the AGPLv3 License" from octoprint.util import deprecated from . import serializing # noqa: F401 from .encoding import JsonEncoding, dumps, loads # noqa: F401 dump = deprecated( "dump has been renamed to dumps, please adjust your implementation", includedoc="dump has been renamed to dumps", since="1.8.0", )(dumps)