Developing a plugin

Plugin name

The plugin name must have a maxium size of 8 characters and must be lower case. This restriction is linked to xPL.

Version number

The plugin version must respect the rules for version numbers.

Files

At least, a plugin consist of the following files :

  • src/domogik_packages/xpl/bin/myplg.py : the python part that is the link between the library and the xPL network. This is called the bin part.
  • src/domogik_packages/xpl/lib/myplg.py : the python part that contains all the functions/classes related to the device/service the plugin will handle. This is called the library part.
  • src/share/domogik/plugins/myplg.json : a json file which describe the plugin and the features it handles.
  • src/domogik_packages/conversions/myplg.py : the python part that contains one class called myplgConversions with static methods to convert specifick info

Sometimes a plugin could also need some data files.

With which part should I begin ?

First, you should develop the library. When the library is functionnal or when some features of your library are functionnal, you could start the bin part. Then, create the json file and finish with the needed xml files for stats and url2xpl.

Rules

There are some rules to follow when developing a plugin.

xPL

  • A plugin has to reply with a xpl-trig command to a xpl-cmnd command in order the emitter to be sure that the plugin received the xPL command.
  • Use xpl-trig for an event or when a value (status, temperature, etc) changes and xpl-stat when it doesn’t change.
  • Read the xPL specification document.

Physical devices and interfaces

  • When a plugin needs to open a device (ex : /dev/ttyUSB0), the plugin should :
    • have an option in order to be able to choose the device path value.
    • try to open device only one time.
    • if the device couldn’t be opened :
      • the plugin must log an explicit message in the plugin log file.
      • the plugin should exit itself (use of self.force_leave()).
  • A plugin don’t have to be launched as root : if necessary, the plugin documentation must explain how to access the plugin device without needing to be root : this should be done by providing a sample udev rule in the json file.

Web services or web features

  • A plugin that needs a web service should not keep a connexion (user session) always open : being connected 24H/24 to a webservice is not a good idea in terms of bandwidtch, security, ... So, the plugin should connect to the web service only when it needs it.

Python code

Coding conventions

See PEP 8

Specific rules we follow when coding

Private instance variables that cannot be accessed except from inside an object don’t exist in Python. Although there are recommended conventions they still remain more or less evasive. So we decided to adopt our own conventions following as close as possible the PEP8 ones.

Classes:

  • Private attributes / methods are systematically prefixed by a double underscore : __foo
  • If we consider that the attribute / method can be accessed by derivated objects then we prefix them by a simple underscore : _foo

Modules (no class)

  • Use a simple underscore for variables / functions that shouldn’t be directly accessible. So they won’t be usable when the module will be imported.

Configuration management

The plugin configuration is described in the json file. Example:

"configuration": [
    {
        "default": "False",
        "description": "Automatically start plugin at Domogik startup",
        "id": "0",
        "interface": "no",
        "key": "startup-plugin",
        "optionnal": "no",
        "options": [],
        "type": "boolean"
    },
    {
        "default": "/dev/teleinfo",
        "description": "Teleinfo device (ex : /dev/ttyUSB0 for an usb model)",
        "id": "1",
        "interface": "no",
        "key": "device",
        "optionnal": "no",
        "options": [],
        "type": "string"
    },
    {
        "default": "60",
        "description": "Interval between each request (seconds)",
        "id": "2",
        "interface": "no",
        "key": "interval",
        "optionnal": "no",
        "options": [],
        "type": "number"
    }
],

The first configuration key must always be startup-plugin. For each plugin, this key is checked on Domogik startup. If it is set to True, the plugin will be started on Domogik startup.

The other configuration keys will be read by your plugin on plugin startup. Example:

def __init__(self):
    # initialize the plugin
    XplPlugin.__init__(self, name='teleinfo')
    # get the config object
    self._config = Query(self.myxpl, self.log)
    # read the configuration elements
    device = self._config.query('teleinfo', 'device')
    interval = self._config.query('teleinfo', 'interval')
    ...

Multi interfaces plugins

Some plugins will need to handle several interfaces. For each of these interfaces you may need to configure several parameters. This is the case of the yweather plugin were you may want to check the yeather from several places in the world.

For each interface, one parameter can be used as a device address. Example for yweather : you set the numeric city code as city and you set the city name as device. When creating a device, you will just have to fill the device address with the city name.

Json example:

"configuration": [
    ...
    {
        "default": null,
        "description": "City code. See http://weather.yahoo.com to find code",
        "id": "4",
        "interface": "yes",
        "key": "city",
        "optionnal": "no",
        "options": [],
        "type": "string"
    },
    {
        "default": null,
        "description": "Device address for this city (ex : weather_home)",
        "id": "5",
        "interface": "yes",
        "key": "device",
        "optionnal": "no",
        "options": [],
        "type": "string"
    }

To read the configuration of each interface you had to loop on the query config and use an index. Example:

def __init__(self):
    # initialize the plugin
    XplPlugin.__init__(self, name='yweather')

    .... get config elements not linked to the interfaces ....

    # create an object to store the interfaces
    self.cities = {}
    # set the index and start looping
    num = 1
    loop = True
    while loop == True:
        # try to get the values for each key of the interface
        city_code = self._config.query('yweather', 'city-%s' % str(num))
        device = self._config.query('yweather', 'device-%s' % str(num))
        # if one mandatory value is not *None*, store the configuration element.
        # else, end the loop
        if city_code != None:
            self.cities[city_code] = { "device" : device }
            num = num + 1
        else:
            loop = False

    ...

Change a configuration value from the plugin

A plugin can change its configuration values. You should avoid to do this if you can as the configurations values should be set by the user!

Example: for a plugin foo and a configuration key named bar:

self._config.set('foo', 'bar', 'newvalue')

Log features

In a plugin there are dedicated function to log data in the log files.

Log levels

There are several log levels allowed :

  • info : important messages that you want always to be displayed in a log file. Examples : device successfully opened, configuration display at startup, etc.
  • debug : all other messages that could help to debug a feature. Don’t hesit to be verbose in debug level (it is not activated by default on officiel releases).
  • warning : something not normal occurs but this will not affect the plugin (or slightly).
  • error : an error which affects the plugin occurs. Some error may be followed by a plugin exit.

Functions

To log, just use one of the following functions:

self.log.error("this is an error message")
self.log.warning("this is a warning message")
self.log.info("this is an info message")
self.log.debug("this is a debug message")

The self.log object is created when you call XplPlugin.__init__(...)

Things to know

There is no need to write a dedicated log line to indicate that plugin is starting. The call to XplPlugin.__init__ will do this like this:

2010-06-18 10:48:08,585 domogik-myplugin INFO ----------------------------------
2010-06-18 10:48:08,585 domogik-myplugin INFO Starting plugin 'myplugin' (new manager instance)

Advanced usage

When you want to use more than one log file, you can declare a new logger like this:

log_new = logger.Logger('myplugin-newlog')
self._log_new = log_new.get_logger()
self._log_new.info("This is an info")

Make sure the plugin will be able to stop correctly

There are a few things to avoid to make sure your plugin will stop correctly : * No while True * No sleep(xx)

The first one will never end, the second one will hang during xx seconds even if we asked the plugin to stop.

All the plugins have a method get_stop() provided by the XplPlugin class that they extends from. This method returns a threading.Event instance which is set when the plugin is asked to stop.

While replacement

Replace:

while True:
    do_something()

With:

while not self.get_stop().isSet():
    do_something()

Sleep replacement

Replace:

sleep(60)

With:

self.get_stop().wait(60)

which will exit as soon as the Event is set, whereas sleep() will wait the whole time.

Use get_stop() in the library part of the plugin

If you need to use such a feature in the library part of your plugin, you can do something like :

In bin/myplugin.py:

from domogik.xpl.common import XplPlugin
from domogik.xpl.lib import myplugin

class MyPlugin(XplPlugin):
    def __init__(self):
        self._mypluginlib = myplugin.MyPluginLib(some, other, param, self.get_stop())

In lib/myplugin.py:

class MyPluginLib:
    def __init__(self, some, other, param, stop):
        self._some = some
        self._other = other
        self._param = param
        self._stop = stop

    def some_method(self):
        while not self._stop.isSet():
            do_something()
            self._stop.wait(60)

Extra things to do when the plugin is shutting down

If you need to do some more things when your plugins shutdowns (close devices, files, ...), you can use the add_stop_cb method provided by the XplPlugin class. For example: bin/myplugin.py:

from domogik.xpl.common import XplPlugin
from domogik.xpl.lib import myplugin

class MyPlugin(XplPlugin):
    def __init__(self):
        self._mypluginlib = myplugin.MyPluginLib(some, other, param)
        self.add_stop_cb(self._mypluginlib.stop)

lib/myplugin.py:

class MyPluginLib:
    def __init__(self, some, other, param):
        self._some = some
        self._other = other
        self._param = param

    def stop(self):
        #do something here, like close your serial port or network socket or ...

How to call a function / method when something happens in the lib module

Calling a function/method of the bin part from the library is very usefull : typically an event is fired and catched by the lib module of your plugin. Typically an event is fired and catched by the lib module of your plugin. Then you’d like to send a xPL message. How can you do that?

You have to use a callback function, here is how to do it:

In your bin module, you define a function that sends a xPL message:

def send_xpl(self, param1, param2):
    msg = XplMessage(...)
    ...

In your bin module, somewhere you instantiate your lib class, and pass the callback method:

myLib = MyLib(p1, p2, cb = self.send_xpl)

So in your lib module you will have something like:

class MyLib:
    def __init__(self, p1, p2, cb):
        self.callback = cb
        ....

    def my_method_that_will_send_a_xpl_message():
        ...
        self.callback(param1, param2)

Creating a timer in the plugin

To create a timer, first you must import:

from domogik.xpl.common.xplconnector import XplTimer
import threading

Then, in the code do this:

timer = XplLTimer(60, my_callback_function, self.myxpl)
timer.start()

60 is the interval between each call of my_callback_function. The function will be called first on timer startup, and then each 60 seconds.

When you want to stop the timer, just do:

timer.stop()

Best practices

Test plugins in a nutshell

During a plugin development, instead of launching a plugin from the user interface, you can launch it from a nutshell:

$ cd src/domogik/xpl/bin
$ ./myplugin.py -f

The -f option makes the plugin to be launched in foreground