=================== 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 :doc:`rules for version numbers `. Files ===== At least, a plugin consist of the following files : * :doc:`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. * :doc:`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. * :doc:`src/share/domogik/plugins/myplg.json ` : a json file which describe the plugin and the features it handles. * :doc:`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 :doc:`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 :doc:`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 :doc:`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