Sample Plugin v1.5

This documentation is valid vor SmartHomeNG verseions beyond v1.4.2. Id does not work on v1.4.2 and below.

On this page you find files for writing a new plugin. The plugin consist of a file with Python code (__init__.py), a metadata fila (plugin.yaml) and a documentation file (README.md). A skeleton of the three files is shown below.

A formatted version of the sample README.md can be found here:

A raw version of the README.md for copy and paste can be found below the Python source code.

The meta data file:

plugin.yaml
# Metadata for the plugin
plugin:
    # Global plugin attributes
#    type: interface                # plugin type (gateway, interface, protocol, system, web)
    description:
        de: 'Beispiel Plugin für SmartHomeNG v1.5 und höher'
        en: 'Sample plugin for SmartHomeNG v1.5 and up'
    maintainer: msinn
#    tester:                        # Who tests this plugin?
#    keywords: iot xyz
#    documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin        # url of documentation (wiki) page
#    support: https://knx-user-forum.de/forum/supportforen/smarthome-py

    version: 1.5.0                  # Plugin version
    sh_minversion: 1.4              # minimum shNG version to use this plugin
#    sh_maxversion:                 # maximum shNG version to use this plugin (leave empty if latest)
    multi_instance: False           # plugin supports multi instance
    classname: SamplePlugin         # class containing the plugin

#parameters:
    # Definition of parameters to be configured in etc/plugin.yaml
    
#item_attributes:
    # Definition of item attributes defined by this plugin
    

The source code:

__init__.py
#!/usr/bin/env python3
# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab
#########################################################################
#  Copyright 2018 <AUTHOR>                                        <EMAIL>
#########################################################################
#  This file is part of SmartHomeNG.   
#
#  Sample plugin for new plugins to run with SmartHomeNG version 1.4 and
#  upwards.
#
#  SmartHomeNG is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  SmartHomeNG is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with SmartHomeNG. If not, see <http://www.gnu.org/licenses/>.
#
#########################################################################

from lib.module import Modules

from lib.model.smartplugin import *


class SamplePlugin(SmartPlugin):
    """
    Main class of the Plugin. Does all plugin specific stuff and provides
    the update functions for the items
    """
    
    PLUGIN_VERSION='1.5.0'

    def __init__(self, sh, *args, **kwargs):
        """
        Initalizes the plugin. The parameters describe for this method are pulled from the entry in plugin.conf.

        :param sh:  **Deprecated**: The instance of the smarthome object. For SmartHomeNG versions **beyond** 1.3: **Don't use it**! 
        :param *args: **Deprecated**: Old way of passing parameter values. For SmartHomeNG versions **beyond** 1.3: **Don't use it**!
        :param **kwargs:**Deprecated**: Old way of passing parameter values. For SmartHomeNG versions **beyond** 1.3: **Don't use it**!
        
        If you need the sh object at all, use the method self.get_sh() to get it. There should be almost no need for
        a reference to the sh object any more.
        
        The parameters *args and **kwargs are the old way of passing parameters. They are deprecated. They are imlemented
        to support oder plugins. Plugins for SmartHomeNG v1.4 and beyond should use the new way of getting parameter values:
        use the SmartPlugin method get_parameter_value(parameter_name) instead. Anywhere within the Plugin you can get
        the configured (and checked) value for a parameter by calling self.get_parameter_value(parameter_name). It
        returns the value in the datatype that is defined in the metadata.
        """
        self.logger = logging.getLogger(__name__)

        # get the parameters for the plugin (as defined in metadata plugin.yaml):
        #   self.param1 = self.get_parameter_value('param1')
        
        # cycle time in seconds, only needed, if hardware/interface needs to be 
        # polled for value changes (maybe you want to make it a plugin parameter?)
        self._cycle = 60

        # Initialization code goes here

        # On initialization error use:
        #   self._init_complete = False
        #   return

        # The following part of the __init__ method is only needed, if a webinterface is being implemented:
        
        # if plugin should start even without web interface
        self.init_webinterface()

        # if plugin should not start without web interface
        # if not self.init_webinterface():
        #     self._init_complete = False
            
        return


    def run(self):
        """
        Run method for the plugin
        """        
        self.logger.debug("Plugin '{}': run method called".format(self.get_fullname()))
        # setup scheduler for device poll loop
        self.scheduler_add(__name__, self.poll_device, cycle=self._cycle)
        # self.sh.scheduler.add(__name__, self.poll_device, cycle=self._cycle)   # for shNG before v1.4

        self.alive = True
        # if you need to create child threads, do not make them daemon = True!
        # They will not shutdown properly. (It's a python bug)


    def stop(self):
        """
        Stop method for the plugin
        """
        self.logger.debug("Plugin '{}': stop method called".format(self.get_fullname()))
        self.alive = False


    def parse_item(self, item):
        """
        Default plugin parse_item method. Is called when the plugin is initialized.
        The plugin can, corresponding to its attribute keywords, decide what to do with
        the item in future, like adding it to an internal array for future reference
        :param item:    The item to process.
        :return:        If the plugin needs to be informed of an items change you should return a call back function
                        like the function update_item down below. An example when this is needed is the knx plugin
                        where parse_item returns the update_item function when the attribute knx_send is found.
                        This means that when the items value is about to be updated, the call back function is called
                        with the item, caller, source and dest as arguments and in case of the knx plugin the value
                        can be sent to the knx with a knx write function within the knx plugin.
        """
        if self.has_iattr(item.conf, 'foo_itemtag'):
            self.logger.debug("Plugin '{}': parse item: {}".format(self.get_fullname(), item))

        # todo
        # if interesting item for sending values:
        #   return self.update_item


    def parse_logic(self, logic):
        """
        Default plugin parse_logic method
        """
        if 'xxx' in logic.conf:
            # self.function(logic['name'])
            pass


    def update_item(self, item, caller=None, source=None, dest=None):
        """
        Item has been updated
        
        This method is called, if the value of an item has been updated by SmartHomeNG.
        It should write the changed value out to the device (hardware/interface) that 
        is managed by this plugin.
        
        :param item: item to be updated towards the plugin
        :param caller: if given it represents the callers name
        :param source: if given it represents the source
        :param dest: if given it represents the dest
        """
        if caller != self.get_shortname():
            # code to execute, only if the item has not been changed by this this plugin:
            logger.info("Update item: {}, item has been changed outside this plugin".format(item.id()))



            if self.has_iattr(item.conf, 'foo_itemtag'):
                self.logger.debug("Plugin '{}': update_item was called with item '{}' from caller '{}', source '{}' and dest '{}'".format(self.get_fullname(), item, caller, source, dest))
            pass


    def poll_device(self):
        """
        Polls for updates of the device
        
        This method is only needed, if the device (hardware/interface) does not propagate
        changes on it's own, but has to be polled to get the actual status.
        It is called by the scheduler.
        """
        # # get the value from the device
        # device_value = ...
        #
        # # find the item(s) to update:
        # for item in self.sh.find_items('...'):
        #
        #     # update the item
        #     item(device_value, self.get_shortname())
        pass


    def init_webinterface(self):
        """"
        Initialize the web interface for this plugin

        This method is only needed if the plugin is implementing a web interface
        """
        try:
            self.mod_http = Modules.get_instance().get_module('http')   # try/except to handle running in a core version that does not support modules
        except:
             self.mod_http = None
        if self.mod_http == None:
            self.logger.error("Plugin '{}': Not initializing the web interface".format(self.get_shortname()))
            return False
        
        import sys
        if not "SmartPluginWebIf" in list(sys.modules['lib.model.smartplugin'].__dict__):
            self.logger.warning("Plugin '{}': Web interface needs SmartHomeNG v1.5 and up. Not initializing the web interface".format(self.get_shortname()))
            return False

        # set application configuration for cherrypy
        webif_dir = self.path_join(self.get_plugin_dir(), 'webif')
        config = {
            '/': {
                'tools.staticdir.root': webif_dir,
            },
            '/static': {
                'tools.staticdir.on': True,
                'tools.staticdir.dir': 'static'
            }
        }
        
        # Register the web interface as a cherrypy app
        self.mod_http.register_webif(WebInterface(webif_dir, self), 
                                     self.get_shortname(), 
                                     config, 
                                     self.get_classname(), self.get_instance_name(),
                                     description='')
                                   
        return True


# ------------------------------------------
#    Webinterface of the plugin
# ------------------------------------------

import cherrypy
from jinja2 import Environment, FileSystemLoader

class WebInterface(SmartPluginWebIf):


    def __init__(self, webif_dir, plugin):
        """
        Initialization of instance of class WebInterface
        
        :param webif_dir: directory where the webinterface of the plugin resides
        :param plugin: instance of the plugin
        :type webif_dir: str
        :type plugin: object
        """
        self.logger = logging.getLogger(__name__)
        self.webif_dir = webif_dir
        self.plugin = plugin
        self.tplenv = self.init_template_environment()


    @cherrypy.expose
    def index(self, reload=None):
        """
        Build index.html for cherrypy
        
        Render the template and return the html file to be delivered to the browser
            
        :return: contents of the template after beeing rendered 
        """
        tmpl = self.tplenv.get_template('index.html')
        # add values to be passed to the Jinja2 template eg: tmpl.render(p=self.plugin, interface=interface, ...)
        return tmpl.render(p=self.plugin)

The Web interface (template file):

The template file has up to five content blocks to be filled with data of the plugin.

  1. Data for the heading on the right side {% block headtable %}
  2. Tab 1 of the body of the page {% block bodytab1 %}
  3. Tab 2 of the body of the page {% block bodytab2 %}
  4. Tab 3 of the body of the page {% block bodytab3 %}
  5. Tab 4 of the body of the page {% block bodytab4 %}

The number of bodytab blocks that is to be displayed is defined by the template statement {% set tabcount = 4 %}

templates/index.html
{% extends "base_plugin.html" %}

{% set logo_frame = false %}

{% block headtable %}   
<table class="table table-striped table-hover">
    <tbody>
        <tr>
            <td class="py-1"><strong>Prompt 1</strong></td>
            <td class="py-1">{% if 1 == 2 %}{{ _('Ja') }}{% else %}{{ _('Nein') }}{% endif %}</td>
            <td class="py-1" width="50px"></td>
            <td class="py-1"><strong>Prompt 4</strong></td>
            <td class="py-1">{{ _('Wert 4') }}</td>
            <td class="py-1" width="50px"></td>
        </tr>
        <tr>
            <td class="py-1"><strong>Prompt 2</strong></td>
            <td class="py-1">{{ _('Wert 2') }}</td>
            <td></td>
            <td class="py-1"><strong>Prompt 5</strong></td>
            <td class="py-1">-</td>
            <td></td>
        </tr>
        <tr>
            <td class="py-1"><strong>Prompt 3</strong></td>
            <td class="py-1">-</td>
            <td></td>
            <td class="py-1"><strong>Prompt 6</strong></td>
            <td class="py-1">-</td>
            <td></td>
        </tr>
    </tbody>
</table>
{% endblock headtable %}


<!--
    Additional buttons for the web interface (if any are needed) - displayed below the headtable-section
-->
{% block buttons %}
{% if 1==2 %}
    <form action="" method="post">
    <button id="btn1" class="btn btn-shng btn-sm" name="learn" type="submit" value="on"><i class="fas fa-wifi"></i>&nbsp;&nbsp;&nbsp;Mach was&nbsp;</button>
    </form>
{% endif %}
{% endblock %}

<!--
    Define the number of tabs for the body of the web interface (1 - 3)
-->
{% set tabcount = 4 %}


<!--
    Set the tab that will be visible on start, if another tab that 1 is wanted (1 - 3)
-->
{% if item_count==0 %}
    {% set start_tab = 2 %}
{% endif %}


<!--
    Content block for the first tab of the Webinterface
-->
{ % set tab1title = "<strong>" ~ p.get_shortname() ~ " Items</strong> (" ~ item_count ~ ")" % }
{% block bodytab1 %}
<div class="container-fluid m-2">
    {{ _('Hier kommt der Inhalt des Webinterfaces hin.') }}
</div>
{% endblock bodytab1 %}


<!--
    Content block for the second tab of the Webinterface
-->
{ % set tab2title = "<strong>" ~ p.get_shortname() ~ " Geräte</strong> (" ~ device_count ~ ")" % }
{% block bodytab2 %}
{% endblock bodytab2 %}


<!--
    Content block for the third tab of the Webinterface
    If wanted, a title for the tab can be defined as:
        {  % set tab3title = "<strong>" ~ p.get_shortname() ~ " Geräte</strong>" % }

    It has to be defined before (and outside) the block bodytab3
-->
{% block bodytab3 %}
{% endblock bodytab3 %}


<!--
    Content block for the fourth tab of the Webinterface
    If wanted, a title for the tab can be defined as:
        {  % set tab4title = "<strong>" ~ p.get_shortname() ~ " Geräte</strong>" % }

    It has to be defined before (and outside) the block bodytab4
-->
{% block bodytab4 %}
{% endblock bodytab4 %}

The multi-language support file:

locale.yaml
plugin_translations:
    # Translations for the plugin specially for the web interface
    'Wert 2':         {'de': '=', 'en': 'Value 2'}
    'Wert 4':         {'de': '=', 'en': 'Value 4'}

    # Alternative format for translations of longer texts:
    'Hier kommt der Inhalt des Webinterfaces hin.':
        de: '='
        en: 'Here goes the content of the web interface.'

The following file outlines the minimum documentation a plugin should have. This README file should be written in English.

README.md
# Sample Plugin <- put the name of your plugin here

#### Version 1.x.y

Describe the purpose of the plugin right here. (What is the plugin good for?)

## Change history

If you want, you can add a change history here:

### Changes Since version 1.x.x

- Fixed this

### Changes Since version 1.x.w

- Added that feature


## Requirements

List the requirements of your plugin. Does it need special software or hardware?

### Needed software

* list
* the
* needed
* software

Including Python modules and SmartHomeNG modules

### Supported Hardware

* list
* the
* supported
* hardware

## Configuration

### plugin.yaml

Please provide a plugin.yaml snippet for your plugin with ever option your plugin supports. Optional attributes should be commented out.

```yaml
My:
   class_name: MyPlugin
   class_path: plugins.myplugin
   host: 10.10.10.10
#   port: 1010
```

Please provide a description of the attributes.
This plugin needs an host attribute and you could specify a port attribute which differs from the default '1010'.

### items.yaml

List and describe the possible item attributes.

#### my_attr

Description of the attribute(s)...

#### my_attr2

#### Example

Please provide an item configuration with every attribute and usefull settings.

```yaml
# items/my.yaml

someroom:
    mydevice:
        type: bool
        my_attr: setting
```

### logic.yaml
If your plugin support item triggers as well, please describe the attributes like the item attributes.


## Methods
If your plugin provides methods for logics. List and describe them here...

### method1(param1, param2)
This method enables the logic to send param1 and param2 to the device. You could call it with `sh.my.method1('String', 2)`.

### method2()
This method does nothing.