Beispielplugin mit MQTT-Unterstützung
Dieser Abschnitt zeigt ein Beispielplugin mit MQTT-Unterstützung. Es kann mit oder ohne Webinterface umgesetzt werden.
Das komplette Plugin mit allen Dateien kann auf github unter https://github.com/smarthomeng/smarthome im /dev
-Ordner gefunden werden.
- Note:
Diese Dokumentation bezieht sich auf Versionen ab v1.7.0. Sie gilt nicht für Versionen vor v1.7.0
Auf dieser Seiten finden sich Dateien und Anregungen für neue Plugins, die MQTT nutzen. Das Plugin selbst besteht aus einer Datei mit Python-Code (__init__.py), einer Datei mit Metadaten (plugin.yaml) und der Dokumentation (user_doc.rst). Ein Satz Beispieldateien wird weiter unten gezeigt.
Eine formatierte Version der Beispieldokumentation findet sich hier: user_doc.rst
Eine Version im Quelltext als Grundlage für eigene Dokumentationen ist unterhalb der Python-Quellcodes verfügbar.
Die Metadaten-Datei:
# 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.8 und höher'
en: 'Sample plugin using MQTT protocol for SmartHomeNG v1.8 and up'
maintainer: msinn
# tester: # Who tests this plugin?
state: develop # Initial 'develop'. change to 'ready' when done with development
# keywords: iot xyz
# documentation: '' # An url to optional plugin doc - NOT the url to user_doc!!!
# support: https://knx-user-forum.de/forum/supportforen/smarthome-py
version: 1.0.0 # Plugin version
# these min/max-versions MUST be given in quotes, or e.g. 3.10 will be interpreted as 3.1 (3.1 < 3.9 < 3.10)
sh_minversion: '1.10' # minimum shNG version to use this plugin
# sh_maxversion: '1.11' # maximum shNG version to use this plugin (omit if latest)
# py_minversion: '3.10' # minimum Python version to use for this plugin
# py_maxversion: '4.25' # maximum Python version to use for this plugin (omit if latest)
multi_instance: false # plugin supports multi instance
restartable: unknown # plugin supports stopping and starting again, must be implemented
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 to toggle plugin execution, remove if not wanted
# (needs plugin to be restartable)
pause_item:
type: str
default: ''
description:
de: 'Item, um die Ausführung des Plugins zu steuern'
en: 'item for controlling plugin execution'
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)
Der Quelltext:
#!/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 MqttPlugin
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.0.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__()
# if you want to use an item to toggle plugin execution, enable the
# definition in plugin.yaml and uncomment the following line
#self._pause_item_path = self.get_parameter_value('pause_item')
# Initialization code goes here
# On initialization error use:
# self._init_complete = False
# return
# if plugin should start even without web interface
self.init_webinterface(WebInterface)
return
def run(self):
"""
Run method for the plugin
"""
self.logger.dbghigh(self.translate("Methode '{method}' aufgerufen", {'method': 'run()'}))
self.alive = True
# let the plugin change the state of pause_item
if self._pause_item:
self._pause_item(False, self.get_fullname())
# start subscription to all topics
self.start_subscriptions()
def stop(self):
"""
Stop method for the plugin
"""
self.logger.dbghigh(self.translate("Methode '{method}' aufgerufen", {'method': 'stop()'}))
self.alive = False
# let the plugin change the state of pause_item
if self._pause_item:
self._pause_item(True, self.get_fullname())
# if you use schedulers, this stops all schedulers the plugin has started.
#self.scheduler_remove_all()
# 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.
"""
# check for pause item
if item.property.path == self._pause_item_path:
self.logger.debug(f'pause item {item.property.path} registered')
self._pause_item = item
self.add_item(item, updating=True)
return self.update_item
if self.has_iattr(item.conf, 'foo_itemid'):
self.logger.debug(f"parse item: {item.property.path}")
# 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
"""
# check for pause item
if item is self._pause_item:
if caller != self.get_shortname():
self.logger.debug(f'pause item changed to {item()}')
if item() and self.alive:
self.stop()
elif not item() and not self.alive:
self.run()
return
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(f"Update item: {item.property.path}, item has been changed outside this plugin")
if self.has_iattr(item.conf, 'foo_itemtag'):
self.logger.debug(
f"update_item was called with item {item.property.path} from caller {caller}, source {source} and dest {dest}")
pass
Das Webinterface (template file):
Das Template hat bis zu fünf Inhaltsblöcke, die mit den Daten des Plugins befüllt werden können:
Kopfdaten auf der rechten Seite
{% block headtable %}
Tab 1 der Hauptteils der Seite
{% block bodytab1 %}
Tab 2 der Hauptteils der Seite
{% block bodytab2 %}
Tab 3 der Hauptteils der Seite
{% block bodytab3 %}
Tab 4 der Hauptteils der Seite
{% block bodytab4 %}
Die Anzahl der anzuzeigenden bodytab
-Blöcke wird mit der folgenden Anweisung festgelegt:
{% set tabcount = 4 %}
{% 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>
$(document).ready( function () {
/*
loading defaults from /modules/http/webif/gstatic/datatables/datatables.defaults.js
You can copy that file, put it in your plugin directory, rename the "bind" function and
trigger that function here instead of datatables_defaults if you want to change the behaviour.
Of course you can also overwrite defaults by putting the option declarations in {} below.
*/
$(window).trigger('datatables_defaults');
try {
/*
Copy this part for every datatable on your page. Adjust options if necessary.
pageLength and pageResize should be included as they are to adjust it based on the plugin settings
*/
table = $('#maintable').DataTable( {
/* If you want to define your own columnDefs options (e.g. for hiding a column by default), use the concat function shown here.
*/
columnDefs: [{ "targets": [2], "className": "value"}].concat($.fn.dataTable.defaults.columnDefs),
});
}
catch (e) {
console.warn("Datatable JS not loaded, showing standard table without reorder option " + e);
}
});
</script>
<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'], 'maintable', 10));
shngInsertText (device+'_powerState', objResponse[device]['powerState'], 'maintable', 10));
-->
}
$('#maintable').DataTable().draw(false);
}
}
</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> {{ _('nach Devices suchen') }} </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 %}
<!-- remove this div if not needed -->
<div class="mb-2">
{{ _('Hier kommt der Inhalt des Webinterfaces hin.') }} (optional)
</div>
<table id="maintable" class="dataTableAdditional">
<thead>
<tr>
<th></th>
<th>Item</th>
<th>Item Path</th>
<th>Item Value</th>
<th>Topic In/Out</th>
<th>Last Update</th>
<th>Last Change</th>
</tr>
</thead>
{% for item in items %}
{% if p.has_iattr(item.conf, 'mqtt_topic_in') or p.has_iattr(item.conf, 'mqtt_topic_out') %}
<tr>
<td></td>
<td class="py-1">{{ item._path }}</td>
<td class="py-1">{{ item._type }}</td>
<td id="{{ item.id() }}_value" class="py-1">{{ item() }}</td>
<td class="py-1">{% if p.has_iattr(item.conf, 'mqtt_topic_in') %}<strong>in : </strong>{{ p.get_iattr_value(item.conf, 'mqtt_topic_in') }}<br>{% endif %}
{% if p.has_iattr(item.conf, 'mqtt_topic_out') %}<strong>out: </strong>{{ p.get_iattr_value(item.conf, 'mqtt_topic_out') }}{% endif %}</td>
<td id="{{ item.id() }}_last_update" class="py-1">{{ item.last_update().strftime('%d.%m.%Y %H:%M:%S') }}</td>
<td id="{{ item.id() }}_last_change" class="py-1">{{ item.last_change().strftime('%d.%m.%Y %H:%M:%S') }}</td>
</tr>
{% endif %}
{% endfor %}
</table>
<!-- remove this div if not needed -->
<div class="mb-2">
Etwaige Informationen unterhalb der Tabelle (optional)
</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 %}
Die Datei für Mehrsprachigkeit:
# 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.'
Die Dokumentation:
Die folgende Datei skizziert den Mindestumfang für die Plugin-Dokumentation.
.. index:: Plugins; Mqtt-Pluginname (in Kleinbuchstaben)
.. index:: Mqtt-Pluginname (in Kleinbuchstaben)
====================================
Mqtt-Pluginname (in Kleinbuchstaben)
====================================
Hier sollte eine allgemeine Beschreibung stehen, wozu das Plugin gut ist (was es tut).
.. image:: webif/static/img/plugin_logo.png
:alt: plugin logo
:width: 300px
:height: 300px
:scale: 50 %
:align: left
Anforderungen
=============
Anforderungen des Plugins auflisten. Werden spezielle Soft- oder Hardwarekomponenten benötigt?
Notwendige Software
-------------------
* die
* benötigte
* Software
* auflisten
Dies beinhaltet Python- und SmartHomeNG-Module
Unterstützte Geräte
-------------------
* die
* unterstütze
* Hardware
* auflisten
|
Konfiguration
=============
.. comment Den Text **Pluginname (in Kleinbuchstaben)** durch :doc:`/plugins_doc/config/pluginname` ersetzen
Die Plugin Parameter, die Informationen zur Item-spezifischen Konfiguration des Plugins und zur Logik-spezifischen
Konfiguration sind unter **Pluginname (in Kleinbuchstaben)** beschrieben.
Dort findet sich auch die Dokumentation zu Funktionen, die das Plugin evtl. bereit stellt.
Funktionen
----------
<Hier können bei Bedarf ausführliche Beschreibungen zu den Funktionen dokumentiert werden.>
<Sonst diesen Abschnitt löschen>
|
Beispiele
=========
Hier können ausführlichere Beispiele und Anwendungsfälle beschrieben werden. (Sonst ist der Abschnitt zu löschen)
|
Web Interface
=============
Die Datei ``dev/sample_plugin/webif/templates/index.html`` sollte als Grundlage für Webinterfaces genutzt werden. Um Tabelleninhalte nach Spalten filtern und sortieren zu können, muss der entsprechende Code Block mit Referenz auf die relevante Table ID eingefügt werden (siehe Doku).
SmartHomeNG liefert eine Reihe Komponenten von Drittherstellern mit, die für die Gestaltung des Webinterfaces genutzt werden können. Erweiterungen dieser Komponenten usw. finden sich im Ordner ``/modules/http/webif/gstatic``.
Wenn das Plugin darüber hinaus noch Komponenten benötigt, werden diese im Ordner ``webif/static`` des Plugins abgelegt.
|
Version History
===============
In diesem Abschnitt kann die Versionshistorie dokumentiert werden, falls der Plugin Autor dieses möchte. Diese Abschnitt
ist optional.
Hinweis
Das in früheren Versionen verwendete README-Format für die Dokumentation von Plugins ist veraltet. Ein Großteil der Dokumentation ist in die Metadaten-Dokumentation in plugin.yaml übergegangen. Die restliche Dokumentation sollte nur noch im user_doc-Format erfolgen.
Soweit möglich, sollten bestehende README im Rahmen von Aktualisierungen in entsprechende user_doc überführt werden.