Automatische Updates der Daten im Webinterface new

Um die Daten im Webinterface zu aktualisieren, sendet die Webseite periodische AJAX-Anfragen an das Plugin und verarbeitet die zurückgelieferten Informationen, indem die neuen Daten in die DOM-Elemente der Webseite eingetragen werden.

Um automatische Updates zu implementieren, müssen die folgenden Elemente hinzugefügt werden:

  • In der Klasse WebInterface des Plugins muss die Methode get_data_html() implementiert bzw. erweitert werden, um die gewünschten Daten zu liefern.

  • Die DOM-Elemente (z.B. <td>-Elemente im headtable-Block oder in den bodytab?-Blöcken), welche die aktualisierten Daten erhalten sollen, müssen jeweils eine eindeutige ID erhalten.

  • Im HTML-Template muss die JavaScript-Funktion handleUpdatedData() implementiert bzw. erweitert werden.

  • Die Template-Variable update_interval muss auf das gewünschte Update-Intervall (in Millisekunden) gesetzt werden.

Erweitern der Python-Methode get_data_html()

Die Klasse WebInterface im Plugin Code muss so erweitert werden, dass sie die für das Update erforderlichen Daten zusammenstellt und als Dictionary zurückgibt:

class WebInterface(SmartPluginWebIf):

    def __init__(self, webif_dir, plugin):

        ...

    @cherrypy.expose
    def index(self, scan=None, test=None, reload=None):

        ...

    @cherrypy.expose
    def get_data_html(self, dataSet=None):
        """
        Return data to update the webpage

        For the standard update mechanism of the web interface, the dataSet to return the data for is None

        :param dataSet: Dataset for which the data should be returned (standard: None)
        :return: dict with the data needed to update the web page.
        """
        if dataSet == 'overview':
            # get the new data from a variable _webdata
            data = self.plugin._webdata
            try:
                data = json.dumps(data)
                return data
            except Exception as e:
                self.logger.error(f"get_data_html exception: {e}")
        if dataSet is None:
            # get the new data
            data = {}
            data['fromip'] = 'fromip': self.plugin.fromip)

            data['item'] = {}
            for i in self.plugin.items:
                data['item'][i]['value'] = self.plugin.getitemvalue(i)

            # return it as json the the web page
            try:
                return json.dumps(data)
            except Exception as e:
                self.logger.error(f"get_data_html exception: {e}")

        return {}

Die optionale Möglichkeit ein dataSet kann genutzt werden, z.B. Daten in unterschiedlichen Zyklen zu aktualisieren (z.B. für Daten, deren Ermittlung eine längere Zeit in Anspruch nimmt) oder nur bestimmte Daten anzufordern. Dieses Feature ist im database Plugin implementiert und kann dort im Detail angesehen werden.

IDs an DOM-Elemente zuweisen

Normalerweise sieht das headtable wie folgt aus:

{% block headtable %}
    <table class="table table-striped table-hover" style="min-width:600px;">
        <tbody>
            <tr>
                <td class="py-1"><strong>Scanne von IP</strong></td>
                <td class="py-1">{{ p.fromip }}</td>
                ...
            </tr>

            ...

        </tbody>
    </table>
{% endblock headtable %}

Bei Tabellen im bodytab werden die einzelnen Datenzeilen beim Rendern durch die for-Schleife befüllt:

{% block **bodytab1** %}
        <table id="maintable" class="dataTableAdditional">
            <thead>
                <tr>
                    <th></th>
                    <th>{{ _('Item') }}</th>
                    <th>{{ _('Typ') }}</th>
                    <th>{{ _('knx_dpt') }}</th>
                    <th>{{ _('Wert') }}</th>
                </tr>
            </thead>
            <tbody>
                {% for item in items %}
                    <tr>
                        <td></td>
                        <td class="py-1">{{ item._path }}</td>
                        <td class="py-1">{{ item._type }}</td>
                        <td class="py-1">{{ item.conf['knx_dpt'] }}</td>
                        <td class="py-1">{{ item._value }}</td>
                    </tr>
                {% endfor %}
            </tbody>
        </table>
{% endblock **bodytab1** %}

Um die Werte in die <td>-Elemente schreiben zu können, nachdem die Webseite erstellt wurde, müssen die <td>-Elemente jeweils mit einer ID ergänzt werden. Um sicherzustellen, dass die ID in Wertetabellen eindeutig sind, wird die for-Schleifenvariable (hier: der Item Name) verwendet. Es ist wichtig, bei Datentabellen (nicht bei normalen Tabellen!) pro Zeile eine leere Zelle einzufügen! Bei headtables sollten leere Spalten vermieden werden.

{% block headtable %}
    <table class="table table-striped table-hover" style="min-width:600px;">
        <tbody>
            <tr>
                <td class="py-1"><strong>Scanne von IP</strong></td>
                <td id="fromip" class="py-1">{{ p.fromip }}</td>
                ...
            </tr>
            ...
        </tbody>
    </table>
{% endblock headtable %}

...

{% block **bodytab1** %}
        <table id="maintable" class="dataTableAdditional">
            <thead>
                <tr>
                    <th></th>
                    ...
                    <th class="value">{{ _('Wert') }}</th>
                </tr>
            </thead>
            <tbody>
                {% for item in items %}
                    <tr>
                        <td></td>
                        ...
                        <td id="{{ item }}_value" class="py-1">{{ item._value }}</td>
                    </tr>
                {% endfor %}
            </tbody>
        </table>
{% endblock **bodytab1** %}

Jetzt können die DOM-Elemente über die IDs fromip und <item>_value angesprochen werden.

Warnung

Damit die Anzeige und Adaption der Datatables einwandfrei funktioniert, ist es elementar, den Aufbau sauber und exakt aus dem Sampleplugin zu übernehmen. So müssen leere Zellen am Anfang jeder Zeile eingefügt werden. Eine Angabe von Klassen ist nicht nötig, da dies automatisch passiert, wobei die Klasse dataTableAdditional gegebenenfalls zu einem schnelleren Laden führen kann.

Erweitern der JavaScript-Funktion handleUpdatedData()

Das Webinterface ruft regelmäßig eine Methode des Plugins auf, um aktualisierte Daten zu erhalten. Wenn die Daten empfangen wurden, werden sie an die JavaScript-Funktion handleUpdatedData() der Webseite übergeben. Diese Funktion weist dann die neuen Daten den jeweiligen DOM-Elementen zu.

Die Funktion handleUpdatedData() ist im Block pluginscripts des HTML-Templates definiert. Das folgende Beispiel weist die neuen Daten dem oben vorgestellten <td>-Element des headtable zu:

{% block pluginscripts %}
<script>
    function handleUpdatedData(response, dataSet=null) {
        if (dataSet === 'devices_info' || dataSet === null) {
            var objResponse = JSON.parse(response);

            shngInsertText('fromip', objResponse['fromip']);
        }
    }
</script>
{% endblock pluginscripts %}

Das nächste Beispiel befüllt dazu analog die <td>-Elemente der Zeilen in der Tabelle im bodytab?. Die Parameter der shngInsertText-Funktion sind dabei wie folgt:

  1. (obligatorisch) ID des HTML Elements, z.B. der Tabellenzelle

  2. (obligatorisch) zu schreibender Wert, wird aus dem objResponse dict gelesen

  3. (optional) Wenn das Element aus Parameter 0 in einer dataTable ist, muss die ID der Tabelle mitgegeben werden

  4. (optional) Möchte man beim Ändern eines Werts einen Highlight-Effekt, kann die Dauer in Sekunden angegeben werden

{% block pluginscripts %}
<script>
    function handleUpdatedData(response, dataSet=null) {
        if (dataSet === 'devices_info' || dataSet === null) {
            var objResponse = JSON.parse(response);

            for (var item in objResponse) {
                shngInsertText(item+'_value', objResponse['item'][item]['value'], null, 2);
                // bei Tabellen mit datatables Funktion sollte die Zeile lauten (ID ersetzen!):
                // shngInsertText(item+'_value', objResponse['item'][item]['value'], 'maintable', 2);
            }
            // Bei Datatables sollte nach der Schleife die gesamte Tabelle ein Mal aktualisert werden
            // $('#maintable').DataTable().draw(false); // ID entsprechend ersetzen
        }
    }
</script>
{% endblock pluginscripts %}

Sortierbare Tabellen

Wie erwähnt muss für das Aktivieren von sortier- und durchsuchbaren Tabellen der entsprechende Script-Block wie in Das Webinterface mit Inhalt füllen unter Punkt 3 beschrieben eingefügt werden. Dabei ist auch zu beachten, dass der zu sortierenden Tabelle eine entsprechende ID gegeben wird (im Beispiel oben maintable).

Damit die neuen Daten auch von datatables.js erkannt und korrekt sortiert werden, ist es wichtig, dem Aufruf shngInsertText die Tabellen-ID als dritten Parameter mitzugeben (im Beispiel ‚maintable‘).

Standardmäßig werden die Spalten automatisch so skaliert, dass sie sich den Inhalten anpassen. Dies kann va. in Kombination mit dem responsive Modul der Datatables zu unerwünschten Ergebnissen führen. Insofern ist es empfehlenswert, bestimmten Spalten eine konkrete Breite vorzugeben. Dazu sollte im Block pluginstyles entsprechender Code eingefügt werden.

{% block pluginstyles %}
<style>
  table th.dpt {
    width: 40px;
  }
  table th.value {
    width: 100px;
  }
</style>
{% endblock pluginstyles %}

Außerdem ist den Spalten die entsprechende Klasse zuzuweisen. Dies ist durch Angabe mittels class-Attribut in den <th> Tags möglich. Alternativ - und der bessere Ansatz - ist es, die Klassen bei der Initialisierung der Tabelle zuzuweisen. Sollte der Inhalt einer Spalte erwartungsgemäß sehr breit sein, kann die Spalte stattdessen auch durch Zuweisen der Klasse „none“ als ausklappbare Informationszeile konfiguriert werden. Die Deklaration der Tabelle im pluginscripts Block hat dabei wie folgt auszusehen, wobei bei targets die interne Nummerierung der Spalten anzugeben ist (0 wäre die erste Tabellenspalte, 1 die zweite, etc.).

table = $('#maintable').DataTable( {
  "columnDefs": [{ "targets": 1, "className": "none"}].concat($.fn.dataTable.defaults.columnDefs)
} );

Hinzufügen von Tabellenzeilen

In manchen Fällen kann es notwendig sein, neue Zeilen einer Tabelle dynamisch hinzuzufügen; beispielsweise, wenn die letzten durchgeführten Commandos oder Logeinträge ergänzt werden sollen. Hierzu ist es nötig, die Funktion handleUpdatedData entsprechend anzupassen.

In der ersten if-Abfrage wird evaluiert, ob bereits ein Element mit entsprechender ID existiert. Falls nicht, wird die Zeile neu angelegt und sanft eingeblendet. Im unten stehenden Code wird zuerst gecheckt, ob es eine Datentabelle mit der ID „maintable“ gibt. In der Zeile if ( $.fn.dataTable.isDataTable('#maintable') ) sowie in der darauf folgenden Zeile muss ‚#maintable‘ durch die tatsächliche ID der zu aktualisierenden Tabelle ersetzt werden. Falls nun eine entsprechende Tabelle auf der Seite gefunden wurde, wird diese als „table_to_update“ definiert (was später für das Hinzufügen einer Zeile mittels row.add genutzt wird).

Durch die Einträge in der Liste table_to_update.row.add( [ item, '' ] ) wird festgelegt, welchen Inhalt die Spalten bekommen sollen. Im Beispielfall wird also der Itemname in die erste Spalte und ein leerer Wert in die zweite Spalte eingetragen. Anschließend wird der zweiten Spalte die relevante ID hinzugefügt, um zukünftig den Wert aktualisieren zu können. Möchte man weiteren Spalten ebenfalls eine ID zuweisen, ist die Codezeile zu kopieren und die Zahl beim Eintrag td:eq(1) entsprechend zu ändern (0 = erste Spalte, 1 = zweite Spalte, etc.). Abschließend wird der leere Wert schließlich mittels shngInsertText aktualisiert und dank Angabe einer Zahl als 4. Parameter x Sekunden lang farblich markiert. Nach der for-Schleife sollte die Tabelle neu gezeichnet werden (siehe Beispiel).

{% block pluginscripts %}
<script>
    function handleUpdatedData(response, dataSet=null) {
        if (dataSet === 'devices_info' || dataSet === null) {
            var objResponse = JSON.parse(response);
            for (var item in objResponse) {
                if (!document.getElementById(item+'_value')) {
                    if ( $.fn.dataTable.isDataTable('#maintable') ) {
                        table_to_update = $('#maintable').DataTable();
                        let newRow = table_to_update.row.add( [ item, '' ] ).draw(false).node();
                        newRow.id = objResponse['item'][item]+"_row";
                        $('td:eq(1)', newRow).attr('id', objResponse['item'][item]+'_value');
                        shngInsertText(item+'_value', objResponse['item'][item]['value'], 'maintable', 5);
                    }
                }
                else
                {
                  shngInsertText(item+'_value', objResponse['item'][item]['value'], 'maintable', 2);
                }

            }
            $('#maintable').DataTable().draw(false);
        }
    }
</script>
{% endblock pluginscripts %}

Hervorheben von Änderungen

Wird über shngInsertText der Inhalt eines HTML Elements aktualisiert, kann dies optional durch einen farbigen Hintergrund hervorgehoben werden. Der jquery UI Effekt switchClass wechselt dabei sanft von einer CSS Klasse zur anderen. Die Dauer des Effekts kann im letzten Parameter des Aufrufs von shngInsertText in Sekunden angegeben werden. Eine Dauer von 0 oder keine Angabe sorgen dafür, dass kein Highlight Effekt ausgeführt wird. Außerdem wird der Effekt auch nicht aktiviert, wenn der vorige Wert ... war (z.B. beim Initialisieren der Tabelle, bevor aktualisierte Werte vom Plugin kommen). Die beiden Klassen sind bereits hinterlegt, können aber in der index.html des Plugin webif im Block pluginStyles bei Bedarf überschrieben werden.

{% block pluginstyles %}
<style>
    .shng_effect_highlight {
      background-color: #FFFFE0;
    }
    .shng_effect_standard {
      background-color: none;
    }
</style>
{% endblock pluginstyles %}

Festlegen von Aktualisierungsintervall, dataSets und weiteren Parametern

Zu Beginn der Templatedatei webif/templates/index.html finden sich die folgenden Zeilen:

{% set update_interval = 0 %}
{% set update_active = false %}
{% set dataSet = 'item_details' %}
{% set update_params = item_id %}
{% set buttons = True %}
{% set autorefresh_buttons = true %}
{% set reload_button = true %}
{% set close_button = true %}
{% set row_count = false %}
{% set initial_update = true %}

Das Intervall wird via update_interval auf den gewünschten Wert in Millisekunden gesetzt. Dabei muss sichergestellt sein, dass das gewählte Intervall lang genug ist, dass die Python-Methode get_data_html() des Plugins die Daten liefern kann, bevor das Intervall abläuft. Wenn nur Daten zurückgegeben werden, die von anderen Routinen und Threads des Plugins bereits bereitgestellt wurden, kann ein Update-Intervall von ca. 1000 ms gewählt werden. Wenn die Python-Methode get_data_html() selbst noch weitere Routinen ausführen muss, sollte das Update-Intervall wahrscheinlich nicht kleiner als 5000 ms sein.

Warnung

Das Intervall darf nicht zu klein sein. Die Dauer MUSS länger sein als die notwendige Zeit zur Ausführung der Python-Methode get_data_html(). Bei datenintensiven Plugins macht es u.U. Sinn, das Intervall abhängig von der Anzahl an Datensätzen festzulegen.

Durch update_active wird festgelegt, ob die automatische Aktualisierung zum Start aktiviert oder deaktiviert sein soll. Dies kann hilfreich sein, um z.B. ein optimales Updateintervall anzugeben, aber dem User zu überlassen, die automatische Aktualisierung einzuschalten. Im Kopfbereich des Web Interfaces ist dazu neben dem „Aktualisieren“-Button sowohl eine Checkbox zum (De)Aktivieren, als auch ein Feld für die Adaptierung des Intervalls vorgesehen.

Möchte man verschiedene dataSets nutzen, kann durch den entsprechenden Parameter dataSet ein entsprechender Name angegeben werden. Außerdem ist es möglich, zusätzliche Parameter zu definieren, die der Methode zur Verfügung gestellt werden soll. Dazu sollte die Methode get_data_html in der webif __init__.py entsprechend angepasst werden. Das vereinfachte Beispiel ist dem Database Plugin entnommen, das zwei Tabs mit verschiedenen Daten anzeigt, die eben auch unterschiedliche Rückmeldungen aus dem Plugin erhalten.

Die Angaben zu den Buttons sind optional und können genutzt werden, um die Schalter und Auto-Refresh Funktionen im Header zu verstecken, wenn sie nicht gebraucht werden.

Durch Definieren der row_count Variable wird beim Anzeigen einer Tabelle automatisch die Anzahl an Datenreihen ermittelt und in die Variable window.row_count gespeichert. Auf diesen Wert kann in einem eigenen Javascript zugegriffen werden. Wird diese Funktion nicht gebraucht, sollte die entsprechende Zeile gelöscht bzw. nicht gesetzt werden.

Wenn initial_update auf „true“ gesetzt ist, werden automatisch beim Laden jeder Seite die Daten mittels get_data_html abgefragt. Dies ist insbesondere dann sinnvoll, wenn anfangs die Tabelle mit Dummy-Daten oder ohne Inhalt befüllt wurde.

@cherrypy.expose
def get_data_html(self, dataSet=None, params=None):
    """
    Return data to update the webpage

    For the standard update mechanism of the web interface, the dataSet to return the data for is None

    :param dataSet: Dataset for which the data should be returned (standard: None)
    :return: dict with the data needed to update the web page.
    """
    self.logger.debug("Page Refresh for Dataset: {}, params: {}".format(dataSet, params))
    if dataSet == 'overview':
        # get the new data
        data = self.plugin._webdata
        try:
            data = json.dumps(data)
            return data
        except Exception as e:
            self.logger.error(f"get_data_html exception: {e}")
    if dataSet == "item_details":
        item_id = params
        if item_id is not None:
            rows = self.plugin.readLogs(item_id, time_start=time_start, time_end=time_end)
        else:
            rows = []
        try:
            data = json.dumps(rows)
            if data:
                return data
            else:
                return None
        except Exception as e:
            self.logger.error(f"get_data_html exception: {e}")

    return {}

Dynamische Anpassung von Aktualisierungsintervall, dataSets und weiteren Parametern

Unter Umständen ist es sinnvoll, diverse Parameter der automatischen Aktualisierung durch ein Script (oder einen Button) anzupassen. Die Parameter werden dabei durch Aufruf von window.refresh.update({}); in Form eines Dictionary aktualisiert. Die möglichen Schlüsselwörter des Dictionaries sind dabei:

  • dataSet: zur Angabe der Inhalte, die vom Plugin angefordert werden sollen

  • update_params: etwaige zusätzliche Parameter für die get_data_html Methode

  • update_interval: das Updateintervall in Millisekunden

  • update_active: Aktivieren oder Deaktivieren der automatischen Aktualisierung

Das folgende fiktive Beispiel zeigt einen Script Block, bei dem die Aktualisierung ausgeschaltet wird, wenn wir den 12.12.2022 haben (was vermutlich wenig Sinn macht und daher angepasst werden sollte).

<!--
This is an example on how to update the page refresh method. You can set the dataSet, update interval, special parameters or (de)activate the auto refresh
In the example the update is deactivated on the 12th of December 2022 (what might make no sense at all)
-->
<script>
  var today = new Date();
  var today_date = String(today.getDate()) + String(today.getMonth() + 1) + today.getFullYear();
  let test_date = "12122022";
  if (today_date === test_date)
      window.refresh.update({dataSet:'test', update_params:'specialitem', update_interval:2000, update_active:false});
</script>