Sample Plugin using MQTT new

This section shows a sample plugin, which uses MQTT. This plugin can be implemented with or without a web interface.

The complete plugins with all files (specially the plugin with web interface has some additional files) can be found on github (www.github.com/smarthomeng/smarthome in the /dev folder.

This documentation is valid for SmartHomeNG versions v1.7.0 and beyond. It does not work on earlier version of SmartHomeNG.

On this page you find files for writing a new plugin. The plugin consists of a file with Python code (__init__.py), a metadata file (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: README.md

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: unknown                   # plugin type (gateway, interface, protocol, system, web)
    description:
        de: 'Beispiel Plugin mit MQTT Protokoll Nutzung für SmartHomeNG v1.7 und höher'
        en: 'Sample plugin using MQTT protocol for SmartHomeNG v1.7 and up'
    maintainer: msinn
#    tester:                         # Who tests this plugin?
    state: develop                  # change to ready when done with development
#    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.7.0                  # Plugin version
    sh_minversion: 1.7              # minimum shNG version to use this plugin
#    sh_maxversion:                 # maximum shNG version to use this plugin (leave empty if latest)
#    py_minversion: 3.6             # minimum Python version to use for this plugin
#    py_maxversion:                 # maximum Python version to use for this plugin (leave empty if latest)
    multi_instance: False           # plugin supports multi instance
    restartable: unknown
    classname: SamplePlugin         # class containing the plugin

parameters:
    # Definition of parameters to be configured in etc/plugin.yaml (enter 'parameters: NONE', if section should be empty)

item_attributes:
    # Definition of item attributes defined by this plugin (enter 'item_attributes: NONE', if section should be empty)

item_structs:
    # Definition of item-structure templates for this plugin (enter 'item_structs: NONE', if section should be empty)

plugin_functions:
    # Definition of plugin functions defined by this plugin (enter 'plugin_functions: NONE', if section should be empty)

logic_parameters:
    # Definition of logic parameters defined by this plugin (enter 'logic_parameters: NONE', if section should be empty)

The source code:

__init__.py
#!/usr/bin/env python3
# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab
#########################################################################
#  Copyright 2020-      <AUTHOR>                                  <EMAIL>
#########################################################################
#  This file is part of SmartHomeNG.
#  https://www.smarthomeNG.de
#  https://knx-user-forum.de/forum/supportforen/smarthome-py
#
#  Sample plugin for new plugins using MQTT to run with SmartHomeNG
#  version 1.7 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.model.mqttplugin import *
from lib.item import Items

from .webif import WebInterface


# If a needed package is imported, which might be not installed in the Python environment,
# add it to a requirements.txt file within the plugin's directory


class SampleMqttPlugin(MqttPlugin):
    """
    Main class of the Plugin. Does all plugin specific stuff and provides
    the update functions for the items
    """

    PLUGIN_VERSION = '1.7.0'    # (must match the version specified in plugin.yaml), use '1.0.0' for your initial plugin Release

    def __init__(self, sh):
        """
        Initalizes the plugin.

        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.

        Plugins have to use the new way of getting parameter values:
        use the SmartPlugin method get_parameter_value(parameter_name). 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.
        """

        # Call init code of parent class (MqttPlugin)
        super().__init__()

        # get the parameters for the plugin (as defined in metadata plugin.yaml):
        # self.param1 = self.get_parameter_value('param1')

        # Initialization code goes here

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

        # if plugin should start even without web interface
        self.init_webinterface()

        return

    def run(self):
        """
        Run method for the plugin
        """
        self.logger.debug("Run method called")

        self.alive = True

        # start subscription to all topics
        self.start_subscriptions()

    def stop(self):
        """
        Stop method for the plugin
        """
        self.logger.debug("Stop method called")
        self.alive = False

        # stop subscription to all topics
        self.stop_subscriptions()

    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_itemid'):
            self.logger.debug("parse item: {}".format(item))

            # subscribe to topic for relay state
            # mqtt_id = self.get_iattr_value(item.conf, 'foo_itemid').upper()
            # payload_type = item.property.type
            # topic = 'shellies/shellyplug-' + mqtt_id + '/relay/0'
            # bool_values = ['off','on']
            # self.add_subscription(topic, payload_type, bool_values, item=item)

            # alternative:
            #   self.add_subscription(topic, payload_type, bool_values, callback=self.on_mqtt_message)
            # and implement callback:
            #   def on_mqtt_message(self, topic, payload, qos=None, retain=None):

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

            # if the item is changed in SmartHomeNG and shall update the mqtt device, enable:
            # 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 self.alive and caller != self.get_shortname():
            # code to execute if the plugin is not stopped
            # and only, if the item has not been changed by this this plugin:
            self.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(
                    "update_item was called with item '{}' from caller '{}', source '{}' and dest '{}'".format(item,
                                                                                                               caller,
                                                                                                               source,
                                                                                                               dest))
            pass

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 %}

<!-- set update_interval to a value > 0 (in milliseconds) to enable periodic data updates -->
{% set update_interval = 0 %}

<!--
    Additional script tag for plugin specific javascript code go into this block
-->
{% block pluginscripts %}
<script>
    function handleUpdatedData(response, dataSet=null) {
        if (dataSet === 'devices_info' || dataSet === null) {
            var objResponse = JSON.parse(response);
            myProto = document.getElementById(dataSet);
            for (var device in objResponse) {
                <!--
                shngInsertText (device+'_source', objResponse[device]['source']);
                shngInsertText (device+'_powerState', objResponse[device]['powerState']);
                -->
            }
        }
    }
</script>
{% endblock pluginscripts %}


{% 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 %}
    <div>
        <button id="btn1" class="btn btn-shng btn-sm" name="scan" onclick="shngPost('', {learn: 'on'})"><i class="fas fa-question"></i>&nbsp;&nbsp;&nbsp;{{ _('nach Devices suchen') }}&nbsp;</button>
    </div>
{% 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
# translations for the web interface
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 MQTT 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 refer to the documentation generated from plugin.yaml metadata.


### items.yaml

Please refer to the documentation generated from plugin.yaml metadata.


### logic.yaml
Please refer to the documentation generated from plugin.yaml metadata.


## Methods
Please refer to the documentation generated from plugin.yaml metadata.


## Examples

If you have extensive examples, you could describe them here.


## Web Interfaces

For building a web interface for a plugin, SmartHomeNG delivers a set of 3rd party components with the HTTP module. 
For addons, etc. that are delivered with the components, see /modules/http/webif/gstatic folder!

The plugin needs further components, they have to be located in the static folder of the plugin's web interface 
folder (webif).