Difference between revisions of "Gateway plugins"

From OpenMotics
Jump to navigation Jump to search
Line 141: Line 141:
  
 
<syntaxhighlight lang="python">
 
<syntaxhighlight lang="python">
    [
+
description =  [
 
         { 'name' : 'hostname', 'type' : 'str',      'description': 'The hostname of the server.' },
 
         { 'name' : 'hostname', 'type' : 'str',      'description': 'The hostname of the server.' },
 
         { 'name' : 'port',    'type' : 'int',      'description': 'Port on the server.' },
 
         { 'name' : 'port',    'type' : 'int',      'description': 'Port on the server.' },

Revision as of 09:13, 8 March 2014

The plugin system on the OpenMotics Gateway allows users to run python code on the gateway. This code can interact with the OpenMotics Master through the webservice, expose new methods on the webservice, receive events for input and output changes and run background tasks.

Writing a plugin

The plugins are written in Python 2.7 and creating a new one is as easy as extending the OMPluginBase. Three class variables should be defined: name (String), version (String with format x.y.z with x,y,z are integers) and interfaces (a list of tuples (interface name, version)). You will find more information about the interface system below.

class MyPlugin(OMPluginBase):
    
    name = 'MyPlugin'
    version = '0.0.1'
    interfaces = [ ('webui', '1.0') ]


The @om_expose decorator

Decorator to expose a method of the plugin class through the webinterface. The url will be /plugins/<plugin-name>/<method>.

Normally an authentication token is required to access the method. The token will be checked and removed automatically when using the following construction:

@om_expose
def method_to_expose(self, ...):
...

It is possible to expose a method without authentication: no token will be required to access the method, this is done as follows:

@om_expose(auth=False)
def method_to_expose(self, ...):
...


The @input_status decorator

Decorator to indicate that the method should receive input status messages. The receiving method should accept one parameter, a tuple of (input, output). Each time an input is pressed, the method will be called.

Important ! This method should not block, as this will result in an unresponsive system. Please use a separate thread to perform complex actions on input status messages.

@input_status
def recv_input_status(self, status):
    (input, output) = status
    ....


The @output_status decorator

Decorator to indicate that the method should receive output status messages. The receiving method should accept one parameter, a list of tuples (output, dimmer value). Each time an output status is changed, the method will be called.

Important ! This method should not block, as this will result in an unresponsive system. Please use a separate thread to perform complex actions on output status messages.

@output_status
def recv_output_status(self, status):
    for s in status:
        (output, dimmer_level) = s
        ....


The @background_task decorator

Decorator to indicate that the method is a background task. A thread running this background task will be started on startup.

@output_status
def run(self):
    ....


The @on_remove decorator

Decorator to indicate that the method should be called just before removing the plugin. This can be used to cleanup files written by the plugin. Note: the plugin package and plugin configuration data will be removed automatically and should not be touched by this method.

@on_remove
def uninstall(self):
    ....


Interfaces

The purpose of the interface system is that generic interface can be defined, those interfaces can be implemented by the plugins. For example an audio interface could define methods for setting the channel, the volume and so on. An concrete implementation of the audio interface can then be created for each type of audio device. This allows for a generic web page for controlling audio devices with different backends for different audio devices.

An interface defines a set of methods on the plugin that are exposed on the webinterface. At the moment only two interfaces are defined:

The webui interface

The webui interface allows the user to expose a html page on the gateway web interface. The gateway web interface shows an extra tab for each plugin that implements the webui interface, the tab contains an iframe with the content of the 'html_index' method.

The webui defines 1 method: 'html_index' that requires authentication and takes no arguments. For example:

@om_expose
def html_index(self):
    return "<html><body>Hello world</body></html>"


The config interface

The config interface provides a generic way to configure a plugin. It exposes a method that returns the configuration description (this contains the fields and types in the configuration), a method for setting and a method for getting the configuration. These methods should return json serialized objects.

The config interfaces contains:

  • get_config_description: returns a list of objects (each object defines one field in the configuration).
  • get_config: returns the configuration object
  • set_config(config): config is the configuration object, returns { 'success' : true }

See the section 'Plugin configuration' below for more information about the configuration and configuration description objects.

Plugin configuration

The PluginConfigController enables the plugin creator to easily implement the 'config' plugin interface.

PluginConfigController

The PluginConfigController is able to verify if a configuration dict matches a description, when a configuration description is provided. The description is a list of dicts, each dict contains the 'name', 'type' and optionally 'description' keys.

These are the basic types: 'str', 'int', 'bool', 'password', these types don't have additional keys. For the 'enum' type the user specifies the possible values in a list of strings in the 'choices' key.

The complex types 'section' and 'nested_enum' allow the creation of lists and conditional elements.

A 'nested_enum' allows the user to create a subsection of which the content depends on the choosen enum value. The 'choices' key should contain a list of dicts with two keys: 'value', the value of the enum and 'content', a configuration description like specified here.

A 'section' allows the user to create a subsection or a list of subsections (when the 'repeat' key is present and true, a minimum number of subsections ('min' key) can be provided when 'repeat' is true. The 'content' key should provide a configuration description like specified above.

An example of a description:

description =  [
        { 'name' : 'hostname', 'type' : 'str',      'description': 'The hostname of the server.' },
        { 'name' : 'port',     'type' : 'int',      'description': 'Port on the server.' },
        { 'name' : 'use_auth', 'type' : 'bool',     'description': 'Use authentication while connecting.' },
        { 'name' : 'password', 'type' : 'password', 'description': 'Your secret password.' },
        { 'name' : 'enumtest', 'type' : 'enum',     'description': 'Test for enum', 'choices': [ 'First', 'Second' ] },
    
        { 'name' : 'outputs', 'type' : 'section', 'repeat' : true, 'min' : 1, 'content' : [
            { 'name' : 'output', 'type' : 'int' }
        ] },

        { 'name' : 'network',  'type' : 'nested_enum', 'choices' : [
            { 'value': 'Facebook',  'content' : [ { 'name' : 'likes', 'type' : 'int' } ] },
            { 'value': 'Twitter',  'content' : [ { 'name' : 'followers', 'type' : 'int' } ] }
        ] }
    ]

The following methods are defined on the PluginConfigChecker:

pcc = PluginConfigChecker(description)

Creates a PluginConfigChecker using a description. If the description is not valid, a PluginException will be thrown.

pcc.check_config(config)

Check if a config is valid for the given description. Raises a PluginException if the config is not valid

Provided methods on OMPluginBase

Putting it all together

Below is the code for a logging plugin that keeps track of the last 100 events (here an event is an input or output change):

from plugins.base import om_expose, input_status, output_status, OMPluginBase, PluginConfigChecker
from datetime import datetime
import simplejson as json

class Logger(OMPluginBase):
    """ This example plugin shows in memory logging of the last input and output status events.
    The plugin can be configured to disable or enable the logging of input and output events.
    """
    
    name = 'Logger'
    version = '1.0.0'
    interfaces = [ ('webui', '1.0'), ('config', '1.0') ]

    config_descr = [
        { 'name' : 'log_inputs',  'type' : 'bool', 'description': 'Log the input data.'  },
        { 'name' : 'log_outputs', 'type' : 'bool', 'description': 'Log the output data.' }
    ]

    default_config = { 'log_inputs' : True, 'log_outputs' : True }

    def __init__(self, webinterface, logger):
        super(Logger, self).__init__(webinterface, logger)
        
        self.__config = self.read_config(Logger.default_config)
        self.__config_checker = PluginConfigChecker(Logger.config_descr)
        
        self.__logs = []

        self.logger("Started Logger plugin")

    def __check_length(self):
        if len(self.__logs) >= 100:
            self.logger("Chopping the logs")
            self.__logs = self.__logs[-100:]

    @input_status
    def recv_input_status(self, status):
        if self.__config['log_inputs']:
            self.__check_length()
            self.__logs.append("%s -- Input pressed: %s -- Output changed: %s" % (datetime.now(), status[0], status[1]))

    @output_status
    def recv_output_status(self, status):
        if self.__config['log_outputs']:
            self.__check_length()
            self.__logs.append("%s -- Outputs on: %s" % (datetime.now(), status))

    @om_expose
    def html_index(self):
        return "<br/>".join(self.__logs)

    @om_expose
    def get_config_description(self):
        return json.dumps(Logger.config_descr)

    @om_expose
    def get_config(self):
        return json.dumps(self.__config)

    @om_expose
    def set_config(self, config):
        config = json.loads(config)
        self.__config_checker.check_config(config)
        self.write_config(config)
        self.__config = config
        return json.dumps({ 'success' : True })


Packaging a plugin

A plugin is packaged as a tgz file containing the content of the plugin package. The plugin class should be defined in main.py and an __init__.py file should be provided. Extra python files can be included in the package. The following commands can be used to create a package and to calculate the md5 sum (required for the plugin installation):

tar czf plugin.tgz main.py __init__.py
md5sum plugin.tgz


Installing a plugin