Benutzerdefinierte OptiCloud Widgets für Anfänger

In diesem Artikel erstellen wir ein einfaches OptiCloud Widget als Einführung in den OptiCloud Widget Editor.

Einrichtung

Um ein benutzerdefiniertes Widget zu erstellen, müssen wir zunächst eine neue Widget-Bibliothek erstellen. Navigieren Sie dazu in der Seitenleiste zu „Administration“ und wählen Sie „Widget-Bibliothek“.  
Klicken Sie im Abschnitt „Widget-Bibliothek“ oben links auf „Erstellen“, um ein neues Bundle zu erstellen. Geben Sie ihm einen Namen (und eine Beschreibung).
Öffnen Sie dieses neue Bundle, indem Sie auf die Schaltfläche „Details anzeigen“ (Info-Symbol) in der Spalte neben seinem Namen klicken.  
Klicken Sie auf „Widget erstellen“ und wählen Sie „Aktueller Wert“ im Popup-Fenster aus.  
Der Bildschirm wird in zwei Bereiche unterteilt:
Der rechte Bereich ist die Widget-Vorschau – hier wird bereits das Standard-Widget angezeigt.  
Im linken Bereich finden die Arbeiten statt. Hier finden Sie alle Teile eines Widgets, unterteilt in Kategorien:
  • „Allgemein“ enthält allgemeine Einstellungen, die festlegen, wie das Widget in Menüs usw. angezeigt wird.
  • „HTML“ enthält den HTML-Code des Widgets.
  • „CSS“ enthält das CSS des Widgets.
  • „Javascript“ enthält den Code, den das Widget ausführt.
  • „Ressourcen“ enthält Optionen zum Hinzufügen und Verwalten externer Ressourcen, auf die das Widget möglicherweise angewiesen ist.
  • „Einstellungsschema“ ist der Bereich, in dem die Einstellungen des Widgets definiert werden.
  • „Datenschlüsseleinstellungen“ ist der Bereich, in dem die Konfiguration für die dem Widget bereitgestellten Datenschlüssel definiert werden kann.


Als ersten Schritt zur Erstellung unseres Widgets löschen wir den gesamten Standardcode und beginnen mit einer leeren Seite. Löschen Sie die Felder „Einstellungen für Selektor“ und „Datenschlüsseleinstellungen-Selektor“ unter „Allgemein“ und löschen Sie den Inhalt von HTML, CSS und Javascript.

HTML

Unser Widget wird in drei Hauptbereiche unterteilt: den Titel, den Inhalt und den Untertitel.

<div class="titlebar">
    <p>Config</p>
</div>
<div class="details">
    <div id="staticMod" class="detailContainer">
        <span class="title">Static Changed</span>
        <span class="data">---</span>
    </div>
    <div id="dynamicMod" class="detailContainer">
        <span class="title">Dynamic Changed</span>
        <span class="data">---</span>
    </div>
    <div id="version" class="detailContainer">
        <span class="title">Version</span>
        <span class="data">---</span>
    </div>
</div>

<div class="metainfo">
    <p>Timestamp of most recent data</p>    
</div>

 

CSS

Wir definieren auch einen Stil für jedes unserer Elemente:
.titlebar {
    display: flex;
    justify-content: center;

    border-bottom: lightgray 1px solid;
    border-radius: 0 0 1em 1em;
    padding: 0 0 0.5em 0;
    margin-bottom: 0.5em;
}

.titlebar p {
    font-size: large;
    margin: unset;
}

.details {
    display: flex;
    flex-direction: column;
    justify-content: center;
    grid-row-gap: 1em;
    padding: 0 1em;
}

.detailContainer {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.details .title {
    font-size: large;
    margin-right: 0.5em;
}

.details .data {
    background-color: lightgrey;
    border-radius: 0.5em;
    align-self: end;

    padding: 0.5em 2.5em;
    min-width: 50%;
    text-align: center;
}

.metainfo {
    margin-top: 1em;

    display: flex;
    justify-content: center;
    position: relative;
}

 


JS (das Wesentliche)

onInit()

Die Methode onInit() wird aufgerufen, wenn unser Widget initialisiert wird. Sie kann verwendet werden, um dynamische HTML-Elemente zu erstellen, Einstellungen anzuwenden und vieles mehr. Für dieses einfache Widget müssen wir jedoch nichts davon tun, daher rufen wir einfach this.onResize() auf.

onResize()

Die Methode onResize() wird immer dann aufgerufen, wenn die Größe unseres Widgets in einem Dashboard geändert wird. Innerhalb dieser Methode können Sie Code ausführen, um die neue Größe anzupassen und sicherzustellen, dass alles weiterhin funktionsfähig und lesbar ist.

onDataUpdated()

Schließlich sind wir beim Kern unseres Widgets angelangt – der Methode onDataUpdated(). Immer wenn neue Daten in der Cloud eintreffen, wird diese Funktion ausgeführt.  

Wir erfassen diese aktualisierten mit Hilfe des Objekts self.ctx.data. Es ist wie folgt strukturiert:
    data = [
        {
            datasource: {...}, // Informationen über die Quelle der Daten
            dataKey: { // Metadaten zu diesem Datenkanal
                name: 'name', // Name des jeweiligen Entitätsattributs/der Zeitreihe
                type: 'timeseries', // Typ des dataKey. Kann „timeseries”, „attribute” oder „function” sein  
                label: 'Sin', // Bezeichnung des dataKey. Wird als Anzeigewert verwendet (z. B. im Legendenbereich des Widgets)
                color: '#ffffff', // Farbe des Schlüssels. Kann vom Widget verwendet werden, um die Farbe der Schlüsseldaten festzulegen (z. B. Linien im Liniendiagramm oder Segmente im Kreisdiagramm).  
                funcBody: "", // gilt nur für Datenquellen vom Typ „function” und mit dem Schlüsseltyp „function”. Definiert den Hauptteil der Funktion zur Generierung simulierter Daten.
                settings: {} // datenschlüsselspezifische Einstellungen mit einer Struktur gemäß dem definierten JSON-Schema für Datenschlüsseleinstellungen. Siehe Abschnitt „Schema für Einstellungen”.
            },
            data: [ // Array von Datenpunkten
                [   // Datenpunkt
                    1498150092317, // Unix-Zeitstempel des Datenpunkts in Millisekunden
                    1, // Wert, kann entweder eine Zeichenfolge, eine Zahl oder ein Boolescher Wert sein  
                ],
                //...
            ]  
        },
        //...
    ]    

Da wir in diesem Fall nach bestimmten Datenschlüsseln suchen, durchlaufen wir alle empfangenen Schlüssel und prüfen, ob ihr Name mit unseren Erwartungen übereinstimmt.
Dazu teilen wir den Namen des Datenschlüssels am Zeichen . und überprüfen dann das letzte Schlüsselwort anhand einer Reihe statischer Zeichenfolgen.  
Während die Namen der Bereiche vom Benutzer definiert werden können, ist der Name des Datenschlüssels selbst konstant und bietet uns eine Möglichkeit, sicherzustellen, dass wir die richtigen Daten an der richtigen Stelle anzeigen.
for (const signal of self.ctx.data) {
        const lastKey = signal.dataKey.name.split('.').pop();
        ...
}

Sobald wir einen Datenschlüssel gefunden haben, der mit dem erwarteten Namen übereinstimmt, verarbeiten wir die Daten in drei einfachen Schritten.
Zunächst speichern wir die Daten (self.ctx.data[i].data[0][1]) in einer Variablen, um die Übersichtlichkeit zu verbessern.  
Im Falle der beiden Zeitstempel übergeben wir die Daten dann an die integrierte Klasse Date(), um problemlos mit dem Zeitstempel arbeiten zu können.  
Im letzten Schritt führen wir zwei Dinge gleichzeitig aus: Wir verwenden JQuery, um das Element zu finden, in dem wir unsere Daten anzeigen möchten, und legen dessen Textinhalt fest.
Um den Textinhalt zu generieren, verwenden wir die Methode toLocaleString() unseres Date-Objekts, um den Zeitstempel basierend auf den Locale-Einstellungen des Benutzers in eine für Menschen lesbare Form zu konvertieren. Beispielsweise sieht ein Benutzer mit amerikanischer Englisch-Ländereinstellung „6/21/2024, 9:09:30 PM“, während ein Benutzer mit deutscher Ländereinstellung „21.6.2024, 21:09:30“ sieht.

Im Fall des Versionsdatenschlüssels prüfen wir zunächst, ob die empfangenen Daten eine leere Zeichenfolge sind. Ist dies nicht der Fall, suchen wir das Element, das die Versionsdaten enthält, und setzen seinen Textinhalt auf die empfangenen Daten. Ist es leer, setzen wir den Inhalt auf einen Platzhalter, um anzuzeigen, dass keine Daten vorhanden sind.
    if (lastKey == 'StaticModified') {
        const staticTimestamp = signal.data[0][1];
        const staticDate = new Date(staticTimestamp);
        self.ctx.$container.find('.details #staticMod .data').text(staticDate.toLocaleString());

    } else if (lastKey == 'DynamicModified') {
        const dynamicTimestamp = signal.data[0][1];
        const dynamicDate = new Date(dynamicTimestamp);
        self.ctx.$container.find('.details #dynamicMod .data').text(dynamicDate.toLocaleString());

    } else if (lastKey == 'Version') {
        const versionString = signal.data[0][1];
        if (versionString != '') {
            self.ctx.$container.find('.details #version .data').text(versionString);    
        } else {
            self.ctx.$container.find('.details #version .data').text("---");
        }
    }


Zu guter Letzt speichern und verarbeiten wir mit jedem empfangenen Datenschlüssel auch dessen Zeitstempel und zeigen diesen im Untertitel an. Auf diese Weise können Benutzer sicherstellen, dass die angezeigten Daten nicht veraltet sind und aktualisiert werden.
let timestamp = new Date(signal.data[0][0]);

const MetaInfoSection = self.ctx.$container.find('.metainfo');
MetaInfoSection.find('p').text(`Timestamp: ${timestamp.toLocaleString()}`);

typeParameters

Die Funktion „typeParameters“ teilt der Webanwendung mit, wie viele Datenquellen und Datenschlüssel das Widget akzeptieren soll. In unserem Fall möchten wir drei Datenschlüssel von einem Gerät verarbeiten, daher legen wir die Parameter entsprechend fest.
self.typeParameters = function() {
    return {
        maxDatasources: 1,
        maxDataKeys: 3
    };
};

Conclusion

Damit ist der Javascript-Teil unseres Widgets fertiggestellt:
self.onInit = function() {
    self.onResize();
};

self.onDataUpdated = function() {
   
    for (const signal of self.ctx.data) {
        // get last part of key name, since that is unchangeable
        const lastKey = signal.dataKey.name.split('.').pop();
       
        if (lastKey == 'StaticModified') {
            const staticTimestamp = signal.data[0][1];
            const staticDate = new Date(staticTimestamp);
            self.ctx.$container.find('.details #staticMod .data').text(staticDate.toLocaleString());
           
        } else if (lastKey == 'DynamicModified') {
            const dynamicTimestamp = signal.data[0][1];
            const dynamicDate = new Date(dynamicTimestamp);
            self.ctx.$container.find('.details #dynamicMod .data').text(dynamicDate.toLocaleString());
           
        } else if (lastKey == 'Version') {
            const versionString = signal.data[0][1];
            if (versionString != '') {
                self.ctx.$container.find('.details #version .data').text(versionString);    
            } else {
                self.ctx.$container.find('.details #version .data').text("---");
            }
        }
       
        // get timestamp of each data point, to show to the user, that data is not outdated
        let timestamp = new Date(signal.data[0][0]);

        const MetaInfoSection = self.ctx.$container.find('.metainfo');
        MetaInfoSection.find('p').text(`Timestamp: ${timestamp.toLocaleString()}`);
    }
};

self.onResize = function() {
    //do nothing
};

self.typeParameters = function() {
    return {
        maxDatasources: 1,
        maxDataKeys: 3
    };
};