Hooks

General Concepts

Hooks are the smaller siblings of mixins, allowing to extend functionality or data processing where a custom mixin type would be too much overhead. Where mixins are based on classes, hooks are based on methods. Like with the mixin implementations, plugins inform OctoPrint about hook handlers using a control property, __plugin_hooks__.

This control property is a dictionary consisting of the implemented hooks’ names as keys and either the hook callback or a 2-tuple of hook callback and order value as value.

Each hook defines a contract detailing the call parameters for the hook handler method and the expected return type. OctoPrint will call the hook with the define parameters and process the result depending on the hook.

An example for a hook within OctoPrint is octoprint.comm.protocol.scripts, which allows adding additional lines to OctoPrint’s GCODE scripts, either as prefix (before the existing lines) or as postfix (after the existing lines).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
self._gcode_hooks = self._pluginManager.get_hooks("octoprint.comm.protocol.scripts")

# ...

for hook in self._gcodescript_hooks:
    try:
        retval = self._gcodescript_hooks[hook](self, "gcode", scriptName)
    except:
        self._logger.exception("Error while processing gcodescript hook %s" % hook)
    else:
        if retval is None:
            continue
        if not isinstance(retval, (list, tuple)) or not len(retval) == 2:
            continue

        def to_list(data):
            if isinstance(data, str):
                data = map(str.strip, data.split("\n"))
            elif isinstance(data, unicode):
                data = map(unicode.strip, data.split("\n"))

            if isinstance(data, (list, tuple)):
                return list(data)
            else:
                return None

        prefix, suffix = map(to_list, retval)
        if prefix:
            scriptLines = list(prefix) + scriptLines
        if suffix:
            scriptLines += list(suffix)

As you can see, the hook’s method signature is defined to take the current self (as in, the current comm layer instance), the general type of script for which to look for additions (“gcode”) and the script name for which to look (e.g. beforePrintStarted for the GCODE script executed before the beginning of a print job). The hook is expected to return a 2-tuple of prefix and postfix if has something for either of those, otherwise None. OctoPrint will then take care to add prefix and suffix as necessary after a small round of preprocessing.

Plugins can easily add their own hooks too. For example, the Software Update Plugin declares a custom hook “octoprint.plugin.softwareupdate.check_config” which other plugins can add handlers for in order to register themselves with the Software Update Plugin by returning their own update check configuration.

If you want your hook handler to be an instance method of a mixin implementation of your plugin (for example since you need access to instance variables handed to your implementation via mixin invocations), you can get this work by using a small trick. Instead of defining it directly via __plugin_hooks__ utilize the __plugin_load__ property instead, manually instantiate your implementation instance and then add its hook handler method to the __plugin_hooks__ property and itself to the __plugin_implementation__ property. See the following example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# coding=utf-8

import octoprint.plugin

class CustomActionCommandPlugin(octoprint.plugin.OctoPrintPlugin):

    def custom_action_handler(self, comm, line, action, *args, **kwargs):
        if not action == "custom":
            return

        self._logger.info("Received \"custom\" action from printer")

__plugin_name__ = "Custom action command"

def __plugin_load__():
    plugin = CustomActionCommandPlugin()

    global __plugin_implementation__
    __plugin_implementation__ = plugin

    global __plugin_hooks__
    __plugin_hooks__ = {"octoprint.comm.protocol.action": plugin.custom_action_handler}

Execution Order

Hooks may also define an order number to allow influencing the execution order of the registered hook handlers. Instead of registering only a callback as hook handler, it is also possible to register a 2-tuple consisting of a callback and an integer value used for ordering handlers. They way this works is that OctoPrint will first sort all registered hook handlers with a order number, taking their identifier as the second sorting criteria, then after that append all hook handlers without a order number sorted only by their identifier.

An example should help clear this up. Let’s assume we have the following plugin ordertest which defines a new hook called octoprint.plugin.ordertest.callback:

Listing 10 ordertest.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import octoprint.plugin

class OrderTestPlugin(octoprint.plugin.StartupPlugin):
    def get_sorting_key(self, sorting_context):
        return 10

    def on_startup(self, *args, **kwargs):
        self._logger.info("############### Order Test Plugin: StartupPlugin.on_startup called")
        hooks = self._plugin_manager.get_hooks("octoprint.plugin.ordertest.callback")
        for name, hook in hooks.items():
            hook()

    def on_after_startup(self):
        self._logger.info("############### Order Test Plugin: StartupPlugin.on_after_startup called")

__plugin_name__ = "Order Test"
__plugin_version__ = "0.1.0"
__plugin_implementation__ = OrderTestPlugin()

And these three plugins defining handlers for that hook:

Listing 11 oneorderedhook.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import logging

 def callback(*args, **kwargs):
     logging.getLogger("octoprint.plugins." + __name__).info("Callback called in oneorderedhook")

 __plugin_name__ = "One Ordered Hook"
 __plugin_version__ = "0.1.0"
 __plugin_hooks__ = {
     "octoprint.plugin.ordertest.callback": (callback, 1)
 }
Listing 12 anotherorderedhook.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import logging

def callback(*args, **kwargs):
    logging.getLogger("octoprint.plugins." + __name__).info("Callback called in anotherorderedhook")

__plugin_name__ = "Another Ordered Hook"
__plugin_version__ = "0.1.0"
__plugin_hooks__ = {
    "octoprint.plugin.ordertest.callback": (callback, 2)
}
Listing 13 yetanotherhook.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import logging

def callback(*args, **kwargs):
    logging.getLogger("octoprint.plugins." + __name__).info("Callback called in yetanotherhook")

__plugin_name__ = "Yet Another Hook"
__plugin_version__ = "0.1.0"
__plugin_hooks__ = {
    "octoprint.plugin.ordertest.callback": callback
}

Both orderedhook.py and anotherorderedhook.py not only define a handler callback in the hook registration, but actually a 2-tuple consisting of a callback and an order number. yetanotherhook.py only defines a callback.

OctoPrint will sort these hooks so that orderedhook will be called first, then anotherorderedhook, then yetanotherhook. Just going by the identifiers, the expected order would be anotherorderedhook, orderedhook, yetanotherhook, but since orderedhook defines a lower order number (1) than anotherorderedhook (2), it will be sorted before anotherorderedhook. If you copy those files into your ~/.octoprint/plugins folder and start up OctoPrint, you’ll see output like this:

[...]
2016-03-24 09:29:21,342 - octoprint.plugins.ordertest - INFO - ############### Order Test Plugin: StartupPlugin.on_startup called
2016-03-24 09:29:21,355 - octoprint.plugins.oneorderedhook - INFO - Callback called in oneorderedhook
2016-03-24 09:29:21,357 - octoprint.plugins.anotherorderedhook - INFO - Callback called in anotherorderedhook
2016-03-24 09:29:21,358 - octoprint.plugins.yetanotherhook - INFO - Callback called in yetanotherhook
[...]
2016-03-24 09:29:21,861 - octoprint.plugins.ordertest - INFO - ############### Order Test Plugin: StartupPlugin.on_after_startup called
[...]

Available plugin hooks

Note

All of the hooks below take at least two parameters, *args and **kwargs. Make sure those are always present in your hook handler declaration. They will act as placeholders if additional parameters are added to the hooks in the future and will allow your plugin to stay compatible to OctoPrint without any necessary adjustments from you in these cases.

octoprint.accesscontrol.appkey

acl_appkey_hook(*args, **kwargs)

By handling this hook plugins may register additional App session key providers within the system.

Overrides this to return your additional app information to be used for validating app session keys. You’ll need to return a list of 3-tuples of the format (id, version, public key).

The id should be the (unique) identifier of the app. Using a domain prefix might make sense here, e.g. org.octoprint.example.MyApp.

version should be a string specifying the version of the app for which the public key is valid. You can provide the string any here, in which case the provided public key will be valid for all versions of the app for which no specific public key is defined.

Finally, the public key is expected to be provided as a PKCS1 string without newlines.

Returns:A list of 3-tuples as described above
Return type:list

octoprint.cli.commands

cli_commands_hook(cli_group, pass_octoprint_ctx, *args, **kwargs)

By providing a handler for this hook plugins may register commands on OctoPrint’s command line interface (CLI).

Handlers are expected to return a list of callables annotated as Click commands to register with the CLI.

The custom MultiCommand instance OctoPrintPluginCommands is provided as parameter. Via that object handlers may access the global Settings and the PluginManager instance as cli_group.settings and cli_group.plugin_manager.

Example:

Registers two new commands, custom_cli_command:greet and custom_cli_command:random with OctoPrint:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# Needs OctoPrint 1.3.x or newer

import click

def clitest_commands(*args, **kwargs):
    @click.command("greet")
    @click.option("--greeting", "-g", default="Hello", help="The greeting to use")
    @click.argument("name", default="World")
    def greet_command(greeting, name):
        """Greet someone by name, the greeting can be customized."""
        click.echo("{} {}!".format(greeting, name))


    @click.command("random")
    @click.argument("name", default="World")
    @click.pass_context
    def random_greet_command(ctx, name):
        """Greet someone by name with a random greeting."""

        greetings = [
            "Hello",
            "Buon giorno",
            "Hola",
            "Konnichiwa",
            "Oh hai",
            "Hey",
            "Salve"
        ]

        from random import randrange
        greeting = greetings[randrange(0, len(greetings))]
        ctx.invoke(greet_command, greeting=greeting, name=name)

    return [greet_command, random_greet_command]


__plugin_hooks__ = {
    "octoprint.cli.commands": clitest_commands
}

Calling octoprint plugins --help shows the two new commands:

$ octoprint plugins --help
Usage: octoprint plugins [OPTIONS] COMMAND [ARGS]...

  Additional commands provided by plugins.

Options:
  --help  Show this message and exit.

Commands:
  custom_cli_command:greet   Greet someone by name, the greeting can be...
  custom_cli_command:random  Greet someone by name with a random greeting.
  softwareupdate:check       Check for updates.
  softwareupdate:update      Apply updates.

Each also has an individual help output:

$ octoprint plugins custom_cli_command:greet --help
Usage: octoprint plugins custom_cli_command:greet [OPTIONS] [NAME]

  Greet someone by name, the greeting can be customized.

Options:
  -g, --greeting TEXT  The greeting to use
  --help               Show this message and exit.

$ octoprint plugins custom_cli_command:random --help
Usage: octoprint plugins custom_cli_command:random [OPTIONS] [NAME]

  Greet someone by name with a random greeting.

Options:
  --help  Show this message and exit.

And of course they work too:

$ octoprint plugins custom_cli_command:greet
Hello World!

$ octoprint plugins custom_cli_command:greet --greeting "Good morning"
Good morning World!

$ octoprint plugins custom_cli_command:random stranger
Hola stranger!

Note

If your hook handler is an instance method of a plugin mixin implementation, be aware that the hook will be called without OctoPrint initializing your implementation instance. That means that none of the injected properties will be available and also the initialize() method will not be called.

Your hook handler will have access to the plugin manager as cli_group.plugin_manager and to the global settings as cli_group.settings. You can have your handler turn the latter into a PluginSettings instance by using octoprint.plugin.plugin_settings_from_settings_plugin() if your plugin’s implementation implements the SettingsPlugin mixin and inject that and the plugin manager instance yourself:

import octoprint.plugin

class MyPlugin(octoprint.plugin.SettingsPlugin):

    def get_cli_commands(self, cli_group, pass_octoprint_ctx, *args, **kwargs):
        import logging

        settings = cli_group._settings
        plugin_settings = octoprint.plugin.plugin_settings_for_settings_plugin("myplugin", self)
        if plugin_settings is None:
            # this can happen if anything goes wrong with preparing the PluginSettings instance
            return dict()

        self._settings = plugin_settings
        self._plugin_manager = cli_group._plugin_manager
        self._logger = logging.getLogger(__name__)

        ### command definition starts here

        # ...

No other platform components will be available - the CLI runs outside of a running, fully initialized OctoPrint server context, so there is absolutely no way to access a printer connection, the event bus or anything else like that. The only things available are the settings and the plugin manager.

Returns:A list of Click commands or groups to provide on OctoPrint’s CLI.
Return type:list

octoprint.comm.protocol.action

protocol_action_hook(comm_instance, line, action, *args, **kwargs)

React to a action command received from the printer.

Hook handlers may use this to react to react to custom firmware messages. OctoPrint parses the received action command line and provides the parsed action (so anything after // action:) to the hook handler.

No returned value is expected.

Example:

Logs if the custom action (// action:custom) is received from the printer’s firmware.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# coding=utf-8

import octoprint.plugin

class CustomActionCommandPlugin(octoprint.plugin.OctoPrintPlugin):

    def custom_action_handler(self, comm, line, action, *args, **kwargs):
        if not action == "custom":
            return

        self._logger.info("Received \"custom\" action from printer")

__plugin_name__ = "Custom action command"

def __plugin_load__():
    plugin = CustomActionCommandPlugin()

    global __plugin_implementation__
    __plugin_implementation__ = plugin

    global __plugin_hooks__
    __plugin_hooks__ = {"octoprint.comm.protocol.action": plugin.custom_action_handler}
Parameters:
  • comm_instance (object) – The MachineCom instance which triggered the hook.
  • line (str) – The complete line as received from the printer, format // action:<command>
  • action (str) – The parsed out action command, so for a line like // action:some_command this will be some_command

octoprint.comm.protocol.gcode.<phase>

This describes actually four hooks:

  • octoprint.comm.protocol.gcode.queuing
  • octoprint.comm.protocol.gcode.queued
  • octoprint.comm.protocol.gcode.sending
  • octoprint.comm.protocol.gcode.sent
protocol_gcodephase_hook(comm_instance, phase, cmd, cmd_type, gcode, subcode=None, *args, **kwargs)

Pre- and postprocess commands as they progress through the various phases of being sent to the printer. The phases are the following:

  • queuing: This phase is triggered just before the command is added to the send queue of the communication layer. This corresponds to the moment a command is being read from a file that is currently being printed. Handlers may suppress or change commands or their command type here. This is the only phase that supports multi command expansion by having the handler return a list, see below for details.
  • queued: This phase is triggered just after the command was added to the send queue of the communication layer. No manipulation is possible here anymore (returned values will be ignored).
  • sending: This phase is triggered just before the command is actually being sent to the printer. Right afterwards a line number will be assigned and the command will be sent. Handlers may suppress or change commands here. The command type is not taken into account anymore.
  • sent: This phase is triggered just after the command was handed over to the serial connection to the printer. No manipulation is possible here anymore (returned values will be ignored). A command that reaches the sent phase must not necessarily have reached the printer yet and it might also still run into communication problems and a resend might be triggered for it.

Hook handlers may use this to rewrite or completely suppress certain commands before they enter the send queue of the communication layer or before they are actually sent over the serial port, or to react to the queuing or sending of commands after the fact. The hook handler will be called with the processing phase, the cmd to be sent to the printer as well as the cmd_type parameter used for enqueuing (OctoPrint will make sure that the send queue will never contain more than one line with the same cmd_type) and the detected gcode command (if it is one).

Defining a cmd_type other than None will make sure OctoPrint takes care of only having one command of that type in its sending queue. Predefined types are temperature_poll for temperature polling via M105 and sd_status_poll for polling the SD printing status via M27.

phase will always match the <phase> part of the implemented hook (e.g. octoprint.comm.protocol.gcode.queued handlers will always be called with phase set to queued). This parameter is provided so that plugins may utilize the same hook for multiple phases if required.

Handlers are expected to return one of the following result variants:

  • None: Don’t change anything. Note that Python functions will also automatically return None if an empty return statement is used or just nothing is returned explicitly from the handler. Hence, the following examples are all falling into this category and equivalent:

    def one(*args, **kwargs):
        print("I return None explicitly")
        return None
    
    def two(*args, **kwargs):
        print("I just return without any values")
        return
    
    def three(*args, **kwargs):
        print("I don't explicitly return anything at all")
    

    Handlers which do not wish to modify (or suppress) cmd or cmd_type at all should use this option.

  • A string with the rewritten version of the cmd, e.g. return "M110". To avoid situations which will be difficult to debug should the returned command be later changed to None (with the intent to suppress the command instead but actually causing cmd and cmd_type to just staying as-is), this variant should be entirely avoided by handlers.

  • A 1-tuple consisting of a rewritten version of the cmd, e.g. return "M110",, or None in order to suppress the command, e.g. return None,. Handlers which wish to rewrite the command or to suppress it completely should use this option.

  • A 2-tuple consisting of a rewritten version of the cmd and the cmd_type, e.g. return "M105", "temperature_poll". Handlers which wish to rewrite both the command and the command type should use this option.

  • “queuing” phase only: A list of any of the above to allow for expanding one command into many. The following example shows how any queued command could be turned into a sequence of a temperature query, line number reset, display of the gcode on the printer’s display and finally the actual command (this example does not make a lot of sense to be quite honest):

    def rewrite_foo(self, comm_instance, phase, cmd, cmd_type, gcode, *args, **kwargs):
        if gcode or not cmd.startswith("@foo"):
            return
    
        return [("M105", "temperature_poll"),    # 2-tuple, command & command type
                ("M110",),                       # 1-tuple, just the command
                "M117 echo foo: {}".format(cmd)] # string, just the command
    
    __plugin_hooks__ = {
        "octoprint.comm.protocol.gcode.queuing": rewrite_foo
    }
    

Note: Only one command of a given cmd_type (other than None) may be queued at a time. Trying to rewrite the cmd_type to one already in the queue will give an error.

Example

The following hook handler replaces all M107 (“Fan Off”, deprecated) with an M106 S0 (“Fan On” with speed parameter) upon queuing and logs all sent M106.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# coding=utf-8

import octoprint.plugin

class RewriteM107Plugin(octoprint.plugin.OctoPrintPlugin):
    def rewrite_m107(self, comm_instance, phase, cmd, cmd_type, gcode, *args, **kwargs):
        if gcode and gcode == "M107":
            cmd = "M106 S0"
        return cmd,

    def sent_m106(self, comm_instance, phase, cmd, cmd_type, gcode, *args, **kwargs):
        if gcode and gcode == "M106":
            self._logger.info("Just sent M106: {cmd}".format(**locals()))

__plugin_name__ = "Rewrite M107"
def __plugin_load__():
    global __plugin_implementation__
    __plugin_implementation__ = RewriteM107Plugin()

    global __plugin_hooks__
    __plugin_hooks__ = {
        "octoprint.comm.protocol.gcode.queuing": __plugin_implementation__.rewrite_m107,
        "octoprint.comm.protocol.gcode.sent": __plugin_implementation__.sent_m106
    }
Parameters:
  • comm_instance (object) – The MachineCom instance which triggered the hook.
  • phase (str) – The current phase in the command progression, either queuing, queued, sending or sent. Will always match the <phase> of the hook.
  • cmd (str) – The GCODE command for which the hook was triggered. This is the full command as taken either from the currently streamed GCODE file or via other means (e.g. user input our status polling).
  • cmd_type (str) – Type of command, e.g. temperature_poll for temperature polling or sd_status_poll for SD printing status polling.
  • gcode (str) – Parsed GCODE command, e.g. G0 or M110, may also be None if no known command could be parsed
  • subcode (str) – Parsed subcode of the GCODE command, e.g. 1 for M80.1. Will be None if no subcode was provided or no command could be parsed.
Returns:

None, 1-tuple, 2-tuple or string, see the description above for details.

octoprint.comm.protocol.gcode.received

gcode_received_hook(comm_instance, line, *args, **kwargs)

Get the returned lines sent by the printer. Handlers should return the received line or in any case, the modified version of it. If the the handler returns None, processing will be aborted and the communication layer will get an empty string as the received line. Note that Python functions will also automatically return None if an empty return statement is used or just nothing is returned explicitly from the handler.

Example:

Looks for the response of an M115, which contains information about the MACHINE_TYPE, among other things.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# coding=utf-8

import logging

def detect_machine_type(comm, line, *args, **kwargs):
    if "MACHINE_TYPE" not in line:
        return line

    from octoprint.util.comm import parse_firmware_line

    # Create a dict with all the keys/values returned by the M115 request
    printer_data = parse_firmware_line(line)

    logging.getLogger("octoprint.plugin." + __name__).info("Machine type detected: {machine}.".format(machine=printer_data["MACHINE_TYPE"]))

    return line

__plugin_name__ = "Detect Machine Data"
__plugin_hooks__ = {
    "octoprint.comm.protocol.gcode.received": detect_machine_type
}
Parameters:
  • comm_instance (MachineCom) – The MachineCom instance which triggered the hook.
  • line (str) – The line received from the printer.
Returns:

The received line or in any case, a modified version of it.

Return type:

str

octoprint.comm.protocol.scripts

protocol_scripts_hook(comm_instance, script_type, script_name, *args, **kwargs)

Return a prefix to prepend and a postfix to append to the script script_name of type type. Handlers should make sure to only proceed with returning additional scripts if the script_type and script_name match handled scripts. If not, None should be returned directly.

If the hook handler has something to add to the specified script, it may return a 2-tuple, with the first entry defining the prefix (what to prepend to the script in question) and the last entry defining the postfix (what to append to the script in question). Both prefix and postfix can be None to signify that nothing should be prepended respectively appended.

The returned entries may be either iterables of script lines or a string including newlines of the script lines (which will be split by the caller if necessary).

Example:

Appends an M117 OctoPrint connected to the configured afterPrinterConnected GCODE script.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# coding=utf-8

def message_on_connect(comm, script_type, script_name, *args, **kwargs):
    if not script_type == "gcode" or not script_name == "afterPrinterConnected":
        return None

    prefix = None
    postfix = "M117 OctoPrint connected"
    return prefix, postfix

__plugin_name__ = "Message on connect"
__plugin_hooks__ = {"octoprint.comm.protocol.scripts": message_on_connect}
Parameters:
  • comm_instance (MachineCom) – The MachineCom instance which triggered the hook.
  • script_type (str) – The type of the script for which the hook was called, currently only “gcode” is supported here.
  • script_name (str) – The name of the script for which the hook was called.
Returns:

A 2-tuple in the form (prefix, postfix) or None

Return type:

tuple or None

octoprint.comm.protocol.temperatures.received

protocol_temperatures_received_hook(comm_instance, parsed_temperatures, *args, **kwargs)

Get the parsed temperatures returned by the printer, allowing handlers to modify them prior to handing them off to the system. Handlers are expected to either return parsed_temperatures as-is or a modified copy thereof.

parsed_temperatures is a dictionary mapping from tool/bed identifier (B, T0, T1) to a 2-tuple of actual and target temperature, e.g. {'B': (45.2, 50.0), 'T0': (178.9, 210.0), 'T1': (21.3, 0.0)}.

This hook can be useful in cases where a printer e.g. is prone to returning garbage data from time to time, allowing additional sanity checking to be applied and invalid values to be filtered out. If a handler returns an empty dictionary or None, no further processing will take place.

Example

The following example shows how to filter out actual temperatures that are outside a sane range of 1°C to 300°C.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# coding=utf-8

def sanitize_temperatures(comm, parsed_temps):
    return dict((k, v) for k, v in parsed_temps.items()
                if isinstance(v, tuple) and len(v) == 2 and is_sane(v[0]))

def is_sane(actual):
    return 1.0 <= actual <= 300.0

__plugin_name__ = "Sanitize Temperatures"
__plugin_hooks__ = {
    "octoprint.comm.protocol.temperatures.received": sanitize_temperatures
}

octoprint.comm.transport.serial.factory

serial_factory_hook(comm_instance, port, baudrate, read_timeout, *args, **kwargs)

Return a serial object to use as serial connection to the printer. If a handler cannot create a serial object for the specified port (and baudrate), it should just return None.

If the hook handler needs to perform state switches (e.g. for autodetection) or other operations on the MachineCom instance, it can use the supplied comm_instance to do so. Plugin authors should keep in mind however that due to a pending change in the communication layer of OctoPrint, that interface will change in the future. Authors are advised to follow OctoPrint’s development closely if directly utilizing MachineCom functionality.

A valid serial instance is expected to provide the following methods, analogue to PySerial’s serial.Serial:

readline(size=None, eol=’n’)
Reads a line from the serial connection, compare serial.Filelike.readline.
write(data)
Writes data to the serial connection, compare serial.Filelike.write.
close()
Closes the serial connection, compare serial.Serial.close.

Additionally setting the following attributes need to be supported if baudrate detection is supposed to work:

baudrate
An integer describing the baudrate to use for the serial connection, compare serial.Serial.baudrate.
timeout
An integer describing the read timeout on the serial connection, compare serial.Serial.timeout.

Example:

Serial factory similar to the default one which performs auto detection of the serial port if port is None or AUTO.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def default(comm_instance, port, baudrate, connection_timeout):
    if port is None or port == 'AUTO':
        # no known port, try auto detection
        comm_instance._changeState(comm_instance.STATE_DETECT_SERIAL)
        serial_obj = comm_instance._detectPort(False)
        if serial_obj is None:
            comm_instance._log("Failed to autodetect serial port")
            comm_instance._errorValue = 'Failed to autodetect serial port.'
            comm_instance._changeState(comm_instance.STATE_ERROR)
            eventManager().fire(Events.ERROR, {"error": comm_instance.getErrorString()})
            return None

    else:
        # connect to regular serial port
        comm_instance._log("Connecting to: %s" % port)
        if baudrate == 0:
            serial_obj = serial.Serial(str(port), 115200, timeout=connection_timeout, writeTimeout=10000, parity=serial.PARITY_ODD)
        else:
            serial_obj = serial.Serial(str(port), baudrate, timeout=connection_timeout, writeTimeout=10000, parity=serial.PARITY_ODD)
        serial_obj.close()
        serial_obj.parity = serial.PARITY_NONE
        serial_obj.open()

    return serial_obj
Parameters:
  • comm_instance (MachineCom) – The MachineCom instance which triggered the hook.
  • port (str) – The port for which to construct a serial instance. May be None or AUTO in which case port auto detection is to be performed.
  • baudrate (int) – The baudrate for which to construct a serial instance. May be 0 in which case baudrate auto detection is to be performed.
  • read_timeout (int) – The read timeout to set on the serial port.
Returns:

The constructed serial object ready for use, or None if the handler could not construct the object.

Return type:

A serial instance implementing implementing the methods readline(...), write(...), close() and optionally baudrate and timeout attributes as described above.

octoprint.filemanager.extension_tree

file_extension_hook(*args, **kwargs)

Return additional entries for the tree of accepted file extensions for uploading/handling by the file manager.

Should return a dictionary to merge with the existing extension tree, adding additional extension groups to machinecode or model types.

Example:

The following handler would add a new file type “x3g” as accepted machinecode format, with extensions x3g and s3g:

1
2
3
4
5
6
def support_x3g_machinecode(*args, **kwargs):
    return dict(
        machinecode=dict(
            x3g=["x3g", "s3g"]
        )
    )

Note

This will only add the supplied extensions to the extension tree, allowing the files to be uploaded and managed through the file manager. Plugins will need to add further steps to ensure that the files will be processable in the rest of the system (e.g. handling/preprocessing new machine code file types for printing etc)!

Returns:The partial extension tree to merge with the full extension tree.
Return type:dict

octoprint.filemanager.preprocessor

file_preprocessor_hook(path, file_object, links=None, printer_profile=None, allow_overwrite=False, *args, **kwargs)

Replace the file_object used for saving added files to storage by calling save().

path will be the future path of the file on the storage. The file’s name is accessible via filename.

file_object will be a subclass of AbstractFileWrapper. Handlers may access the raw data of the file via stream(), e.g. to wrap it further. Handlers which do not wish to handle the file_object

Example

The following plugin example strips all comments from uploaded/generated GCODE files ending on the name postfix _strip.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# coding=utf-8

import octoprint.plugin
import octoprint.filemanager
import octoprint.filemanager.util

from octoprint.util.comm import strip_comment

class CommentStripper(octoprint.filemanager.util.LineProcessorStream):
    def process_line(self, line):
        line = strip_comment(line).strip()
        if not len(line):
            return None
        return line + "\r\n"

def strip_all_comments(path, file_object, links=None, printer_profile=None, allow_overwrite=True, *args, **kwargs):
    if not octoprint.filemanager.valid_file_type(path, type="gcode"):
        return file_object

    import os
    name, _ = os.path.splitext(file_object.filename)
    if not name.endswith("_strip"):
        return file_object

    return octoprint.filemanager.util.StreamWrapper(file_object.filename, CommentStripper(file_object.stream()))

__plugin_name__ = "Strip comments from GCODE"
__plugin_description__ = "Strips all comments and empty lines from uploaded/generated GCODE files ending on the name " \
                         "postfix \"_strip\", e.g. \"some_file_strip.gcode\"."
__plugin_hooks__ = {
    "octoprint.filemanager.preprocessor": strip_all_comments
}
Parameters:
  • path (str) – The path on storage the file_object is to be stored
  • file_object (AbstractFileWrapper) – The AbstractFileWrapper instance representing the file object to store.
  • links (dict) – The links that are going to be stored with the file.
  • printer_profile (dict) – The printer profile associated with the file.
  • allow_overwrite (boolean) – Whether to allow overwriting an existing file named the same or not.
Returns:

The file_object as passed in or None, or a replaced version to use instead for further processing.

Return type:

AbstractFileWrapper or None

octoprint.printer.factory

printer_factory_hook(components, *args, **kwargs)

Return a PrinterInstance instance to use as global printer object. This will be called only once during initial server startup.

The provided components is a dictionary containing the already initialized system components:

  • plugin_manager: The PluginManager
  • printer_profile_manager: The PrinterProfileManager
  • event_bus: The EventManager
  • analysis_queue: The AnalysisQueue
  • slicing_manager: The SlicingManager
  • file_manager: The FileManager
  • app_session_manager: The AppSessionManager
  • plugin_lifecycle_manager: The LifecycleManager
  • user_manager: The UserManager
  • preemptive_cache: The PreemptiveCache

If the factory returns anything but None, it will be assigned to the global printer instance.

If no of the registered factories return a printer instance, the default Printer class will be instantiated.

Parameters:components (dict) – System components to use for printer instance initialization
Returns:The printer instance to use globally.
Return type:PrinterInterface subclass or None

octoprint.server.http.bodysize

server_bodysize_hook(current_max_body_sizes, *args, **kwargs)

Allows extending the list of custom maximum body sizes on the web server per path and HTTP method with custom entries from plugins.

Your plugin might need this if you want to allow uploading files larger than 100KB (the default maximum upload size for anything but the /api/files endpoint).

current_max_body_sizes will be a (read-only) list of the currently configured maximum body sizes, in case you want to check from your plugin if you need to even add a new entry.

The hook must return a list of 3-tuples (the list’s length can be 0). Each 3-tuple should have the HTTP method against which to match as first, a regular expression for the path to match against and the maximum body size as an integer as the third entry.

The path of the route will be prefixed by OctoPrint with /plugin/<plugin identifier>/ (if the path already begins with a / that will be stripped first).

Important

Implementing this hook will make your plugin require a restart of OctoPrint for enabling/disabling it fully.

Example

The following plugin example sets the maximum body size for POST requests against four custom URLs to 100, 200, 500 and 1024KB. To test its functionality try uploading files larger or smaller than an endpoint’s configured maximum size (as multipart request with the file upload residing in request parameter file) and observe the behaviour.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# coding=utf-8

import octoprint.plugin
import flask

class BodySizePlugin(octoprint.plugin.BlueprintPlugin,
                     octoprint.plugin.SettingsPlugin):

    def __init__(self):
        self._sizes = (100, 200, 500, 1024)

    @octoprint.plugin.BlueprintPlugin.route("/upload/<int:size>", methods=["POST"])
    def api_endpoint(self, size):
        if not size in self._sizes:
            return flask.make_response(404)

        input_name = "file"
        keys = ("name", "size", "content_type", "path")

        result = dict(
            found_file=False,
        )
        for key in keys:
            param = input_name + "." + key
            if param in flask.request.values:
                result["found_file"] = True
                result[key] = flask.request.values[param]

        return flask.jsonify(result)

    def bodysize_hook(self, current_max_body_sizes, *args, **kwargs):
        return [("POST", r"/upload/%i" % size, size * 1024) for size in self._sizes]

__plugin_name__ = "Increase upload size"
__plugin_description__ = "Increases the body size on some custom API endpoints"

def __plugin_load__():
    global __plugin_implementation__
    global __plugin_hooks__

    __plugin_implementation__ = BodySizePlugin()
    __plugin_hooks__ = {
        "octoprint.server.http.bodysize": __plugin_implementation__.bodysize_hook
    }
Parameters:current_max_body_sizes (list) – read-only list of the currently configured maximum body sizes
Returns:A list of 3-tuples with additional request specific maximum body sizes as defined above
Return type:list

octoprint.server.http.routes

server_route_hook(server_routes, *args, **kwargs)

Allows extending the list of routes registered on the web server.

This is interesting for plugins which want to provide their own download URLs which will then be delivered statically following the same path structure as regular downloads.

server_routes will be a (read-only) list of the currently defined server routes, in case you want to check from your plugin against that.

The hook must return a list of 3-tuples (the list’s length can be 0). Each 3-tuple should have the path of the route (a string defining its regular expression) as the first, the RequestHandler class to use for the route as the second and a dictionary with keywords parameters for the defined request handler as the third entry.

The path of the route will be prefixed by OctoPrint with /plugin/<plugin identifier>/ (if the path already begins with a / that will be stripped first).

Note

Static routes provided through this hook take precedence over routes defined through blueprints.

If your plugin also implements the BlueprintPlugin mixin and has defined a route for a view on that which matches one of the paths provided via its octoprint.server.http.routes hook handler, the view of the blueprint will thus not be reachable since processing of the request will directly be handed over to your defined handler class.

Important

Implementing this hook will make your plugin require a restart of OctoPrint for enabling/disabling it fully.

Example

The following example registers two new routes /plugin/add_tornado_route/download and /plugin/add_tornado_route/forward in the webserver which roughly replicate the functionality of /downloads/files/local and /downloads/camera/current.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# coding=utf-8

import octoprint.plugin

class TornadoRoutePlugin(octoprint.plugin.SettingsPlugin):

    def route_hook(self, server_routes, *args, **kwargs):
        from octoprint.server.util.tornado import LargeResponseHandler, UrlProxyHandler, path_validation_factory
        from octoprint.util import is_hidden_path

        return [
            (r"/download/(.*)", LargeResponseHandler, dict(path=self._settings.global_get_basefolder("uploads"),
                                                           as_attachment=True,
                                                           path_validation=path_validation_factory(lambda path: not is_hidden_path(path),
                                                                                                   status_code=404))),
            (r"forward", UrlProxyHandler, dict(url=self._settings.global_get(["webcam", "snapshot"]),
                                                    as_attachment=True))
        ]

__plugin_name__ = "Add Tornado Route"
__plugin_description__ = "Adds two tornado routes to demonstrate hook usage"

def __plugin_load__():
    global __plugin_implementation__
    global __plugin_hooks__

    __plugin_implementation__ = TornadoRoutePlugin()
    __plugin_hooks__ = {
        "octoprint.server.http.routes": __plugin_implementation__.route_hook
    }

See also

LargeResponseHandler
Customized tornado.web.StaticFileHandler that allows delivery of the requested resource as attachment and access validation through an optional callback.
UrlForwardHandler
tornado.web.RequestHandler that proxies requests to a preconfigured url and returns the response.
Parameters:server_routes (list) – read-only list of the currently configured server routes
Returns:a list of 3-tuples with additional routes as defined above
Return type:list

octoprint.ui.web.templatetypes

templatetype_hook(template_sorting, template_rules, *args, **kwargs)

Allows extending the set of supported template types in the web interface. This is interesting for plugins which want to offer other plugins to hook into their own offered UIs. Handlers must return a list of additional template specifications in form of 3-tuples.

The first entry of the tuple must be the name of the template type and will be automatically prefixed with plugin_<identifier>_.

The second entry must be a sorting specification that defines how OctoPrint should sort multiple templates injected through plugins of this template type. The sorting specification should be a dict with the following possible entries:

Key Description
key The sorting key within the template config to use for sorting the list of template injections. This may be None in which case no sorting will be taking place. Defaults to name.
add Usually irrelevant for custom template types, only listed for the sake of completeness. The method of adding the sorted list of template injections from plugins to the template injections from the core. May be append to append the list, prepend to prepend the list, or custom_append or custom_prepend to append respectively prepend but going so after preprocessing the entries and order data with custom functions (e.g. to inject additional entries such as the “Plugins” section header in the settings dialog). For custom template types this defaults to append.
custom_add_entries Usually irrelevant for custom template types, only listed for the sake of completeness. Custom preprocessor for the entries provided through plugins, before they are added to the general template entries context variable for the current template type.
custom_add_order Usually irrelevant for custom template types, only listed for the sake of completeness. Custom preprocessor for the template order provided through plugins, before they are added to the general template order context variable for the current template type.

The third entry must be a rule specification in form of a dict which tells OctoPrint how to process the template configuration entries provided by get_template_configs() by providing transformation functions of various kinds:

Key Description
div Function that returns the id of the container for template content if not explicitly provided by the template config, input parameter is the name of the plugin providing the currently processed template config. If not provided this defaults to a lambda function of the form lambda x: "<plugin identifier>_<template type>_plugin_" + x with plugin identifier being the identifier of the plugin providing the additional template type.
template Function that returns the default template filename for a template type to attempt to include in case no template name is explicitly provided by the template config, input parameter is the name of the plugin providing the current processed template config. If not provided this defaults to a lambda function of the form lambda x: x + "_plugin_<plugin identifier>_<template type>.jinja2" with plugin identifier being the identifier of the plugin providing the additional template type.
to_entry Function to transform a template config to the data structure stored in the Jinja context for the injected template. If not provided this defaults to a lambda function returning a 2-tuple of the name value of the template config and the template config itself (lambda data: (data["name"], data))
mandatory A list of keys that must be included in the template config for this template type. Template configs not containing all of the keys in this list will be ignored. Defaults to an empty list.

OctoPrint will provide all template configs for custom template types in the Jinja rendering context in the same way as it provides the template configs for core template types, through the templates context variable which is a dict mapping from the template type name (plugin_<plugin identifier>_<template type> for custom ones) to a dict with entries and order values, the first containing a dict of all registered template configs, the latter an ordered list of all registered template keys of the type in the order they should be rendered. Plugins should iterate over the order list and then render each entry utilizing the template entry as provided for the key in the entries dict (note that this entry will have the format specified through the to_entry section in the template rule).

Example

The example consists of two plugins, one providing a custom template type and the other consuming it.

First the provider:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# coding=utf-8

import octoprint.plugin

class CustomTemplateTypeProvider(octoprint.plugin.TemplatePlugin):

    def add_templatetype(self, current_order, current_rules, *args, **kwargs):
        return [
            ("awesometemplate", dict(), dict(template=lambda x: x + "_awesometemplate.jinja2"))
        ]

__plugin_name__ = "Custom Template Provider"
def __plugin_load__():
    global __plugin_implementation__
    __plugin_implementation__ = CustomTemplateTypeProvider()

    global __plugin_hooks__
    __plugin_hooks__ = {
        "octoprint.ui.web.templatetypes": __plugin_implementation__.add_templatetype
    }
1
2
3
4
5
6
7
8
9
<h3>{{ _('Awesome Template Injections') }}</h3>

{% for key in templates.plugin_custom_template_provider_awesometemplate.order %}
    {% set heading, config = templates.plugin_custom_template_provider_awesometemplate.entries[key] %}
    <div id="{{ config._div }}">
        <h4>{{ heading }}</h4>
        {% include config.template ignore missing %}
    </div>
{% endfor %}

Then the consumer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# coding=utf-8

import octoprint.plugin

class CustomTemplateTypeConsumer(octoprint.plugin.TemplatePlugin):
    def get_template_configs(self):
        if "custom_template_provider" not in self._plugin_manager.enabled_plugins:
            # if our custom template provider is not registered, we'll act as a regular settings plugin
            return [
                dict(type="settings", template="custom_template_consumer_awesometemplate.jinja2")
            ]
        else:
            # else we'll inject ourselves as an awesometemplate type instead - since we named our jinja2 file
            # accordingly we don't have to explicitly define that here, it will be picked up automatically
            return []

__plugin_name__ = "Custom Template Consumer"
__plugin_implementation__ = CustomTemplateTypeConsumer()
1
"Hello World!" from an injected awesome template.
Parameters:
  • template_rules (dict) – read-only dictionary of currently configured template rules
  • template_sorting (dict) – read-only dictionary of currently configured template sorting specifications
Returns:

a list of 3-tuples (template type, rule, sorting spec)

Return type:

list

octoprint.users.factory

user_manager_factory_hook(components, settings, *args, **kwargs)

Return a UserManager instance to use as global user manager object. This will be called only once during initial server startup.

The provided components is a dictionary containing the already initialized system components:

  • plugin_manager: The PluginManager
  • printer_profile_manager: The PrinterProfileManager
  • event_bus: The EventManager
  • analysis_queue: The AnalysisQueue
  • slicing_manager: The SlicingManager
  • file_manager: The FileManager
  • app_session_manager: The AppSessionManager
  • plugin_lifecycle_manager: The LifecycleManager
  • preemptive_cache: The PreemptiveCache

If the factory returns anything but None, it will be assigned to the global userManager instance.

If no of the registered factories return a user manager instance, the class referenced by the config.yaml entry accessControl.userManager will be initialized if possible, otherwise a stock FilebasedUserManager will be instantiated, linked to the default user storage file ~/.octoprint/users.yaml.

Parameters:
  • components (dict) – System components to use for user manager instance initialization
  • settings (SettingsManager) – The global settings manager instance to fetch configuration values from if necessary
Returns:

The userManager instance to use globally.

Return type:

UserManager subclass or None