Um SmartHomeNG einsteigerfreundlicher zu machen, bieten wir ab Version 1.5 die Möglichkeit, Plugins mit eigenen Web-Interfaces (ähnlich dem Backend Plugin) zu versehen. Web-Interface bezeichnet hierbei eine kleine Webseite, die Infos zum Plugin ausgibt oder die Bedienung und Konfiguration vereinfacht.

Existierende Web-Interfaces von Plugins, lassen sich in der Plugin-Liste des Backend-Plugins finden (siehe Artikel Das Backend-Plugin). Ein Web-Interface ist instanzspezifisch, die URL wird dementsprechend aufgebaut.

Die Web-Interfaces des AVM, KNX, Wundergrund und Webservices Plugins sind nur einige Beispiele:
   

Basis hierzu stellt die auf CherryPy (und Jinja2 als Template-Engine) basierende Implementierung im http module dar. Für detaillierte Infos zur Template Syntax wird auf die o.g. Doku von Jinja2 verwiesen. Dieser Artikel setzt Grundkenntnisse darin voraus.

Frameworks

SmartHomeNG bringt dabei für alle Plugins nutzbar eine ganze Reihe an Frameworks bereits mit. Diese finden derzeit vor allem im Backend Plugin ihren Einsatz.

Vorhanden sind per Default (Versionen Stand Release 1.5):

Framework Beschreibung / Link
Bootstrap 4.1.1 Web GUI Framework
http://getbootstrap.com/
Bootstrap Datepicker Widget 1.8.0 https://github.com/uxsolutions/bootstrap-datepicker
Bootstrap Tree View
(selber angepasst auf Bootstrap 4)
https://github.com/jonmiles/bootstrap-treeview
JQuery 3.3.1 Javascript Framework
https://jquery.org
Codemirror 5.39.0 Online Code Editor
https://codemirror.net/
Font Awesome 5.1.0 Icon-Font für Webseiten
http://fontawesome.com

 

Abgelegt sind die Frameworks unter /smarthome/modules/http/webif/gstatic und können direkt in einer Webseite genutzt werden.

Am Beispiel Bootstrap sieht das wie folgt aus:

<script src="/gstatic/bootstrap/js/bootstrap.min.js"></script>

Beispielplugin mit Web-Interface

Das jeweils aktuelle Beispiel für ein Plugin mit Web-Interface findet sich unter /dev/sample_plugin_webif.
In der Methode init_webinterface (als Teil des Plugin-Codes in der Datei __init__.py) wird dabei das Web-Interface initialisiert:


from lib.model.smartplugin import *
[...]
    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
        
        # 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

Diese Methode muss dann natürlich in der def __init__ eingebunden werden:


    def __init__(self, sh, *args, **kwargs):
        [...]
        if not self.init_webinterface():
            self._init_complete = False
        [...]

Die eigentliche Implementierung befindet sich in einer eigenen Klasse, die ebenfalls in der __init__.py definiert wird:


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 = Environment(loader=FileSystemLoader(self.plugin.path_join( self.webif_dir, 'templates' ) ))


    @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')
        return tmpl.render(plugin_shortname=self.plugin.get_shortname(), plugin_version=self.plugin.get_version(),
                           plugin_info=self.plugin.get_info(), p=self.plugin)

Der Ausdruck @cherrypy.exposed legt fest, dass die Methode über http-Requests aufrufbar (also über den Browser zugreifbar) ist und diese beantworten kann. Methoden bei denen dies nicht der Fall sein soll, brauchen diese Angabe nicht.
In self.tplenv.get_template wird das index.html Template geladen, in tmpl.render werden Variablen übergeben und das Template als HTML gerendert.

Tipp: Für ein minimales Plugin Web-Interface reicht es im Übrigen, nur die notwendigen Parameter der tmpl.render Methode zu übergeben und eine minimale index.html zu haben.

Das Plugin gestalten

Wie im nachfolgenden Bild am Beispiel des Web-Interfaces des Database Plugins zu sehen, werden statische Dateien wie etwa Bilder im Ordner static direkt unter dem Pluginordner abgelegt. Die eigentlichen Templates für das Web-Interface des Plugins liegen unter templates.

Einstiegs-Template ist die Datei index.html im templates Ordner, die oben in der Methode index definiert wurde. Das Default Template zu dieser Seite ist wie folgt aufgebaut:

  1. Mittels {% extends "base_plugin.html" %} wird von einer globalen Vorlage für Plugin-Web-Interfaces abgeleitet. Diese ist unter /smarthome/modules/http/webif/gtemplates/base_plugin.html zu finden. Über dieses Basis-Template werden bereits Bootstrap, JQuery, Font Awesome und CodeMirror eingebunden. Die Verwendung des Templates stellt sicher, dass Web-Interfaces für Plugins ein homogenes Look and Feel haben.
  2. Da wir SmartHomeNG-weit auf Bootstrap 4 setzen, bitten wir, die Stilmittel und CSS Klassen von Bootstrap zu verwenden. Details siehe http://getbootstrap.com/
  3. Es gibt eine Reihe an Bereichen, die man nun in seiner index.html individuell ausgestalten kann. Diese sog. Blöcke sind headtable, buttons und bodytab1 bis bodytab4
  4. Ein Block kann im Template wie folgt befüllt werden:
    {% block headtable %}
    ...
    {% endblock headtable %}
  5. Im Fall mehrerer bodytab's, können auch noch tab1title bis tab4title vergeben werden. Wird dies nicht gemacht, werden Default Titel erzeugt.

Nachfolgend sind die Blöcke am Beispiel des Web-Interfaces des AVM Plugins nochmals dargestellt:

Tipps und Tricks

Eigene CSS und JS Dateien einbinden

Will man eigene JavaScript oder CSS Files einbinden, so kann man die Blöcke des base_plugin einfach erweitern. Ein Beispiel wäre wie folgt:

{%- block scripts %}
{{ super() }}
<script src="/gstatic/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js"></script>
<script src="/gstatic/bootstrap-datepicker/dist/locales/bootstrap-datepicker.de.min.js"></script>
<script src="/gstatic/bootstrap-datepicker/dist/locales/bootstrap-datepicker.fr.min.js"></script>
{%- endblock scripts %}

{%- block styles %}
{{ super() }}
<link rel="stylesheet" href="/gstatic/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css" type="text/css"/>
{%- endblock styles %}

Über super() wird der Block eines der übergeordneten Templates, in diesem Fall in der ../modules/http/webif/gtemplates/base.html, die der plugin_base.html übergeordnet ist, aufgerufen. Die nach super() folgenden Teile, werden dem Blockinhalt angehängt. Würde man super() weglassen, würde der Block komplett überschrieben.

Unterschiedliche Seiten / Aktionen über Seitenaufrufe auslösen.

Will man nicht nur eine Seite für das Plugin haben, so kann man entweder analog zur Methode def index(self) in der __init__.py weitere Methoden (und damit Seiten) definieren. In jeder dieser Methoden muss via get_template ein jeweils anderes Templates aus dem templates Ordner des Plugins geladen werden.

Eine Alternative zu diesem Vorgehen ist, nur die index Methode zu verwenden und die Auswahl des Templates in der def index(self) via Kommando-Parameter zu steuern. Neue Parameter kann man einfach in der Methode ergänzen, also bspw. def index(self, cmd=None). Die Vorbelegung mit None ist dafür, dass der Parameter auch weggelassen werden kann.

Über cmd kann man nun im Python Code Aktionen auslösen oder abhängig vom Wert in cmd via tmpl = self.tplenv.get_template('...') andere Templates auswählen. cmd muss in einem Link / Formular als GET-Parameter übergeben werden, als bspw. mit ?cmd=... an eine URL angehängt werden.

Ein Beispiel dazu kann im Web-Interface des Database-Plugin gefunden werden.

Wurde eine Aktion erfolgreich ausgeführt, empfiehlt es sich, die Info in tmpl.render an das Template zu übergeben. Über Bootstrap kann man nun schöne Status-Meldungen anzeigen:

{% if delete_triggered %}
<div class="mb-2 alert alert-success alert-dismissible fade show" role="alert">
    <strong>Löschauftrag für die Einträge von Item ID {{ item_id }} in der Tabelle "log" wurde erfolgreich initiiert!</strong><br/>
    Das Löschen kann noch kurze Zeit dauern, da die Ausführung des Delete Queries erst nach Abschluß der bestehenden Transaktionen erfolgen kann.
    <button type="button" class="close" data-dismiss="alert" aria-label="Close">
        <span aria-hidden="true">&times;</span>
    </button>
</div>
{% endif %}

Internationalisierung

Das Übersetzungssystem von SmartHomeNG kann auch für Plugins genutzt werden. Dazu muss direkt im Pluginverzeichnis eine Datei locale.yaml angelegt werden. In dieser werden Übersetzungen wie folgt definiert:


plugin_translations:
    # Translations for the plugin specially for the web interface
    'Aktionen':        {'de': 'Aktionen', 'en': 'Actions'}
    'Verbunden':       {'de': 'Verbunden', 'en': 'Connected'}
    'Ja':              {'de': 'Ja', 'en': 'Yes'}
    'Nein':            {'de': 'Nein', 'en': 'No'}

Im Template kann nun über die Syntax {{ _('<Schlüssel aus der language.yaml>') }} darauf zugegriffen werden:

{% if check %}{{ _('Ja') }}{% else %}{{ _('Nein') }}{% endif %}

Wir bitten darum, Web-Interfaces für Plugins immer mindestens auf Englisch und Deutsch zu erstellen!

(Die in diesem Artikel verwendeten Screenshots wurden selber erstellt. Das Titelbild ist unter der Creative Commons Zero (CC0) Lizenz veröffentlicht und wurde von www.pexels.com bezogen.)


Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.