Adapter-Entwicklung

In diesem Dokument werden einige Code-Beispiele gesammelt, welche häufig bei der Adapter-Entwicklung benötigt werden.

Gefahr

Diese Code-Beispiele sind NICHT für JavaScript-Code im JavaScript-Adapter gedacht! Dort werden teilweise andere Funktionen bzw. unterschiedliche Signaturen genutzt.

Eine Adapter-Klasse sollte immer von utils.Adapter erben. Die Basis-Klasse ist hier zu finden.

const utils = require('@iobroker/adapter-core');

class MyAdapter extends utils.Adapter {
    // ...
}

Nützliche Eigenschaften

  • this.host -

  • this.FORBIDDEN_CHARS -

  • this.namespace - Der komplette Namespace im Format. z.B. admin.0

  • this.instance - Die Instanznummer als numerischer Wert. z.B. 0

  • this.adapterDir - Der abolute Pfad zum Adapter-Verzeichnis (innerhalb node_modules)

  • this.ioPack - Die io-package.json als Objekt

Die folgenden Eigenschaften werden nur bereitgestellt, wenn useFormatDate: true im Constructor übergeben wird.

  • this.dateFormat

  • this.isFloatComma

  • this.language

  • this.longitude

  • this.latitude

Objekte

Wie Objekte genau aufgebaut sind und welche Eigenschaften sie unterstützten, ist auf der Seite Objekte dokumentiert.

Neues Objekt erstellen (im eigenen Namespace)

Gerenell ist es empfehlenswert, Objekte über die instanceObjects in der io-package.json anzulegen. Falls beim Adapter-Start bekannst ist, welche Objekte angelegt werden müssen, ist dieser Weg zu bevorzugen. Alternativ können Objekte per Code angelegt werden:

await this.setObjectNotExistsAsync('exampleDevice', {
    type: 'device',
    common: {
        name: 'Any name',
        type: 'string',
        role: 'text'
    },
    native: {}
});

Bemerkung

Es ist sinnvoll, den Namen aller Objekte (wenn möglich) direkt Mehrsprachig anzulegen! Dies wird z.B. vom Admin-Adapter und js-controller bereits berücksichtigt.

await this.setObjectNotExistsAsync('exampleDevice', {
    type: 'device',
    common: {
        name: {
            en: 'Any name',
            de: 'Jeder Name',
            ru: 'Любое имя',
            pt: 'Qualquer nome',
            nl: 'Iedere naam',
            fr: 'N\'importe quel nom',
            it: 'Qualche nome',
            es: 'Cualquier nombre',
            pl: 'Jakiekolwiek imię',
            uk: 'Ім\'я',
            'zh-cn': '任何名字'
        },
        type: 'string',
        role: 'text'
    },
    native: {}
});

Rückgabe:

{"id":"xxx.0.exampleDevice"}

Bemerkung

Je allgemeiner die Funktion, desto weniger Prüfungen werden durchgeführt. „setObject“ prüft dabei am wenigsten.

createDevice
createChannel
createState
    setObjectNotExists
        setObjectWithDefaultValue
            setObject

Alle Funktionen gibt es asynchron und mit callback. Jeweils für Objekte im eigenen Namespace und fremde Objekte.

// set[Foreign]Object[Async]
// Erstellt (oder überschreibt) das Objekt
await this.setObjectAsync(id, obj, options);
this.setObject(id, obj, options, callback);

await = this.setForeignObjectAsync(id, obj, options);
this.setForeignObject(id, obj, options, callback)

// Wrapper für set[Foreign]Object
this.setObjectWithDefaultValue(id, obj, options, callback);

// set[Foreign]ObjectNotExists[Async]
// Erstellt das Objekt nur, wenn es nicht existiert
// Wrapper für setObjectWithDefaultValue
await this.setObjectNotExistsAsync(id, obj, options);
this.setObjectNotExists(id, obj, options, callback);

await this.setForeignObjectNotExistsAsync(id, obj, options);
this.setForeignObjectNotExists(id, obj, options, callback);

// Wrapper für setObjectNotExists mit type = 'device'
await this.createDeviceAsync(deviceName, common, _native, options);
this.createDevice(deviceName, common, _native, options, callback);

// Wrapper für setObjectNotExists mit type = 'channel'
await this.createChannelAsync(parentDevice, channelName, roleOrCommon, _native, options);
this.createChannel(parentDevice, channelName, roleOrCommon, _native, options, callback);

// Wrapper für setObjectNotExists mit type = 'state'
await this.createStateAsync(parentDevice, parentChannel, stateName, roleOrCommon, _native, options);
this.createState(parentDevice, parentChannel, stateName, roleOrCommon, _native, options, callback);

Bestehendes Objekt aktualisieren (im eigenen Namespace)

Wird ein Objekt aktualisiert, können geschützte Eigenschaften übergeben werden, welche nicht angefasst werden. Dazu zählt z.B. der Name des Objektes, welcher durch den Nutzer geändert werden kann.

await this.extendObjectAsync(deviceName, {
    type: 'device',
    common: {
        name: {
            en: 'Any name',
            de: 'Jeder Name',
            ru: 'Любое имя',
            pt: 'Qualquer nome',
            nl: 'Iedere naam',
            fr: 'N\'importe quel nom',
            it: 'Qualche nome',
            es: 'Cualquier nombre',
            pl: 'Jakiekolwiek imię',
            uk: 'Ім\'я',
            'zh-cn': '任何名字'
        },
        type: 'string',
        role: 'text'
    },
    native: {}
}, { preserve: { common: ['name'] } } );

Alle Funktionen gibt es asynchron und mit callback. Jeweils für Objekte im eigenen Namespace und fremde Objekte.

// extend[Foreign]Object[Async]
await this.extendObjectAsync(id, obj, options);
this.extendObject(id, obj, options, callback);

await this.extendForeignObjectAsync(id, obj, options);
this.extendForeignObject(id, obj, options, callback);

Objekte lesen

Alle Funktionen gibt es asynchron und mit callback. Jeweils für Objekte im eigenen Namespace und fremde Objekte.

// get[Foreign]Object[Async]
await this.getObjectAsync(id, options, callback);
this.getObject(id, options, callback);

await this.getForeignObject(id, options, callback);
this.getForeignObject(id, options, callback);

await this.findForeignObjectAsync(id, type, options);
this.findForeignObject(id, type, options, callback);
const allObjects = await this.getAdapterObjectsAsync(); // Alle folder, device, channel und state Objekte
this.getAdapterObjects(callback); // Alle folder, device, channel und state Objekte

Objekt View

Möchte man viele Objekte auf einmal aus dem System abfragen, so eignet sich die Funktion getObjectView. Mit dieser Funktion können alle möglichen Objekt-Typen (siehe Objekte) aus der Objekt-Datenbank abgefragt werden.

await getObjectViewAsync('system', 'instance', {
    startkey: 'system.adapter.',
    endkey: 'system.adapter.\u9999'
});

Alle Funktionen gibt es asynchron und mit callback. Jeweils für Objekte im eigenen Namespace und fremde Objekte.

await this.getObjectListAsync(params, options);
this.getObjectList(params, options, callback);

await this.getObjectViewAsync(design, search, params, options);
this.getObjectView(design, search, params, options, callback);

// Wrapper für getObjectView mit search "device"  + namespace filter
await this.getDevicesAsync();
this.getDevices();

// Wrapper für getObjectView mit search "channel" + namespace filter
await this.getChannelsOfAsync(parentDevice, options, callback);
this.getChannelsOf(parentDevice, options, callback);

// Wrapper für getObjectView mit search "state" + namespace filter
await this.getStatesOfAsync(parentDevice, parentChannel, options);
this.getStatesOf(parentDevice, parentChannel, options, callback);

// Wrapper für getObjectView mit search "enum"
await this.getEnumAsync(_enum, options);
this.getEnum(_enum, options, callback);

await this.getEnumsAsync(_enumList, options);
this.getEnums(_enumList, options, callback);

// Wrapper für getObjectView
await this.getForeignObjectsAsync(pattern, type, enums, options);
this.getForeignObjects(pattern, type, enums, options, callback);

Objekt löschen (im eigenen Namespace)

Bemerkung

Der zugehörige State wird ebenfalls gelöscht (falls type = state)

await this.delObjectAsync(deviceName);

Objekt rekursiv löschen (im eigenen Namespace)

Unterstützt seit js-controller Version 2.2.8

if (this.supportsFeature && this.supportsFeature('ADAPTER_DEL_OBJECT_RECURSIVE')) {
    await this.delObjectAsync(deviceName, { recursive: true });
}

Alle Funktionen gibt es asynchron und mit callback. Jeweils für Objekte im eigenen Namespace und fremde Objekte.

// del[Foreign]Object[Async]
await this.delObjectAsync(id, options);
this.delObject(id, options, callback);

await this.delForeignObjectAsync(id, options);
this.delForeignObject(id, options, callback);

// Wrapper für delForeignObjectAsync (mit recursive = true)
await this.deleteDeviceAsync(deviceName, options);
this.deleteDevice(deviceName, options, callback);

// Wrapper für delForeignObjectAsync (mit recursive = true)
await this.deleteChannelAsync(parentDevice, channelName, options);
this.deleteChannel(parentDevice, channelName, options, callback);

// Wrapper für delForeignObjectAsync
await this.deleteStateAsync(parentDevice, parentChannel, stateName, options);
this.deleteState(parentDevice, parentChannel, stateName, options, callback);

Zustände (States)

Wie States genau aufgebaut sind und welche Eigenschaften sie unterstützten, ist auf der Seite Zustände (States) dokumentiert.

Wert schreiben (aktualisieren)

Bemerkung

Es ist darauf zu achten, dass der Datentyp des übergebenen Wertes zum definierten Datentyp auf dem Objekt passt.

await this.setStateAsync('myState', {val: newValue, ack: true});

Alternativ kann der neue Wert auch einzeln übergeben werden. Es ist empfohlen, immer ein komplettes State-Objekt zu übergeben, da dies ansonsten intern aufgebaut wird. Sollte newValue (versehentlich) ein Objekt sein, wird es als „fertiges“ State-Objekt interpretiert, welchem dann wichtige Eigenschaften fehlen werden.

await this.setStateAsync('myState', newValue, true);

Alle Funktionen gibt es asynchron und mit callback. Jeweils für States im eigenen Namespace und fremde States.

// set[Foreign]State[Async]
await this.setStateAsync(id, state, ack, options);
this.setState(id, state, ack, options, callback);

await this.setForeignStateAsync(id, state, ack, options);
this.setForeignState(id, state, ack, options, callback);

Wert schreiben (ändern)

Bemerkung

Es ist darauf zu achten, dass der Datentyp des übergebenen Wertes zum definierten Datentyp auf dem Objekt passt.

await this.setStateChangedAsync('myState', {val: newValue, ack: true});

Alle Funktionen gibt es asynchron und mit callback. Jeweils für States im eigenen Namespace und fremde States.

// set[Foreign]StateChanged[Async]
await this.setStateChangedAsync(id, state, ack, options);
this.setStateChanged(id, state, ack, options, callback);

await this.setForeignStateChangedAsync(id, state, ack, options);
this.setForeignStateChanged(id, state, ack, options, callback);

Wert schreiben (binär)

Geänderte Signaturen seit js-controller 4.0.15 (setForeignBinaryState)

Um Binärdaten in States zu speichern, muss das Objekt vom Typ common.type = 'file' sein. Für mehr Details siehe File Storage.

Alle Funktionen gibt es asynchron und mit callback. Jeweils für States im eigenen Namespace und fremde States.

// set[Foreign]BinaryState[Async]
await this.setBinaryStateAsync(id, binary, options);
this.setBinaryState(id, binary, options, callback);

await this.setForeignBinaryStateAsync(id, binary, options);
this.setForeignBinaryState(id, binary, options, callback);

Wert lesen

Um den aktuellen Zustand eines States zu bekommen, können einzelne Werte aus der Datenbank abgefragt werden:

Bemerkung

Sollte der State ein Alias sein, wird automatisch der State des Verknüpften Objektes zurückgegeben.

const state = await this.getStateAsync('myState');
const value = state ? state.val : undefined;
// ...

// oder
this.getState('myState', (err, state) => {
    if (!err) {
        // Das hat nich geklappt!
    } else {
        const value = state.val;
        // ...
    }
});

Rückgabe:

Es wird ein vollständiges State-Objekt zurückgegeben. Siehe Zustände (States).

Alle Funktionen gibt es asynchron und mit callback. Jeweils für States im eigenen Namespace und fremde States.

// get[Foreign]State[Async]
await this.getStateAsync(id, options);
this.getState(id, options, callback)

await this.getForeignStateAsync(id, options);
this.getForeignStates(id, options, callback);

Wert lesen (mehrere auf einmal)

const states = await this.getStatesAsync('pfad.im.eigenen.namespace.*');

Rückgabe:

Alle Funktionen gibt es asynchron und mit callback. Jeweils für States im eigenen Namespace und fremde States.

// get[Foreign]States[Async]
await this.getStatesAsync(pattern, options);
this.getStates(pattern, options, callback);

await this.getForeignStatesAsync(pattern, options);
this.getForeignStates(pattern, options, callback);

Wert lesen (binär)

Geänderte Signaturen seit js-controller 4.0.15 (getForeignBinaryState)

Um Binärdaten in States zu lesen, muss das Objekt vom Typ common.type = 'file' sein. Für mehr Details siehe File Storage.

Alle Funktionen gibt es asynchron und mit callback. Jeweils für States im eigenen Namespace und fremde States.

// get[Foreign]BinaryState[Async]
await this.getBinaryStateAsync(id, options);
this.getBinaryState(id, options, callback);

await this.getForeignBinaryStateAsync(id, options);
this.getForeignBinaryState(id, options, callback);

Wert löschen

Bemerkung

Das zugehörige Objekt wird nicht gelöscht

Alle Funktionen gibt es asynchron und mit callback. Jeweils für States im eigenen Namespace und fremde States.

// del[Foreign]State[Async]
await this.delStateAsync(id, options);
this.delState(id, options, callback);

await this.delForeignStateAsync(id, options);
this.delForeignState(id, options, callback);

Auf Änderungen reagieren

Um Informationen nicht ständig aus der Datenbank abfragen zu müssen, können einzelne States, Objekte oder auch Dateien (Files) (siehe File Storage) abonniert werden. Der js-controller informiert uns dann über Änderungen.

Zustände (States)

  1. Abonnieren mit subscribe[Foreign]States[Async]

  2. Event-Handler registrieren

  3. Änderungen im Event-Handler auswerten

class MyAdapter extends utils.Adapter {
    /**
    * @param {Partial<utils.AdapterOptions>} [options={}]
    */
    constructor(options) {
        super({
            ...options,
            name: 'my-adapter',
        });

        this.on('ready', this.onReady.bind(this));
        this.on('stateChange', this.onStateChange.bind(this));
    }

    async onReady() {
        // Alle eigenen States abonnieren
        await this.subscribeStatesAsync('*');

        // Einen State aus einem anderen Namespace abonnieren
        await this.subscribeForeignStatesAsync('0_userdata.0.beispiel');
    }

    /**
     * @param {string} id
     * @param {ioBroker.State | null | undefined} state
     */
    onStateChange(id, state) {
        if (state && !state.ack) {

            // Verarbeitung bestätigen
            this.setForeignState(id, { val: state.val, ack: true });
        }
    }
}

Alle Funktionen gibt es asynchron und mit callback. Jeweils für States im eigenen Namespace und fremde States.

// subscribe[Foreign]States[Async]
await this.subscribeStatesAsync(pattern, options);
this.subscribeStates(pattern, options, callback);

await this.subscribeForeignStatesAsync(pattern, options);
this.subscribeForeignStates(pattern, options, callback);

Objekte

  1. Abonnieren mit subscribe[Foreign]Objects[Async]

  2. Event-Handler registrieren

  3. Änderungen im Event-Handler auswerten

class MyAdapter extends utils.Adapter {
    /**
    * @param {Partial<utils.AdapterOptions>} [options={}]
    */
    constructor(options) {
        super({
            ...options,
            name: 'my-adapter',
        });

        this.on('ready', this.onReady.bind(this));
        this.on('objectChange', this.onObjectChange.bind(this));
    }

    async onReady() {
        // Alle eigenen Objekte abonnieren
        await this.subscribeObjectsAsync('*');

        // Ein Objekt aus einem anderen Namespace abonnieren
        await this.subscribeForeignObjectsAsync('0_userdata.0.beispiel');
    }

    /**
     * @param {string} id
     * @param {ioBroker.Object | null | undefined} obj
     */
    onObjectChange(id, obj) {
        if (obj) {
            // Objekt geändert
        } else {
            // Objekt gelöscht
        }
    }
}

Alle Funktionen gibt es asynchron und mit callback. Jeweils für States im eigenen Namespace und fremde States.

// subscribe[Foreign]Objects[Async]
await this.subscribeObjectsAsync(pattern, options);
this.subscribeObjects(pattern, options, callback);

await this.subscribeForeignObjectsAsync(pattern, options);
this.subscribeForeignObjects(pattern, options, callback);

Dateien (Files)

Unterstützt seit js-controller Version 4.1.0

  1. Abonnieren mit subscribeFiles[Async]

  2. Event-Handler registrieren

  3. Änderungen im Event-Handler auswerten

class MyAdapter extends utils.Adapter {
    /**
    * @param {Partial<utils.AdapterOptions>} [options={}]
    */
    constructor(options) {
        super({
            ...options,
            name: 'my-adapter',
        });

        this.on('ready', this.onReady.bind(this));
        this.on('fileChange', this.onFileChange.bind(this));
    }

    async onReady() {
        await this.subscribeFiles(this.namespace);
    }

    /**
     * @param {string} id
     * @param {string} fileName
     * @param {number} size
     */
    onFileChange(id, fileName, size) {

    }
}

Daten formatieren

Um Daten für den Endanwender lesbar zu formatieren, gibt es im Adapter verschiedene Funktionen, welche diese Aufgabe übernehmen:

  • formatDate

  • formatValue

Beide Funktionen beziehen sich im Standard auf die System-Einstellungen (es können ggf. eigene Formatierungs-Optionen übergeben werden).

Bei formatValue wird dieses Format als String (mit 2 Zeichen Länge!) übergeben. Dabei ist das erste Zeichen im String das 1000er-Trennzeichen und das zweite definiert das Komma. Eselsbrücke: Die Reihenfolge ist genauso, wie später im Ergebnis.

Beispiele:

  • this.formatValue(2.43425, 3); - Rückgabe: 2,434

  • this.formatValue(2.53425, 0); - Rückgabe: 3 (wird gerundet!)

  • this.formatValue(1000.123, 2, ‚.,‘); - Rückgabe: 1.000,12

  • this.formatValue(1000.123, 2, ‚,.‘); - Rückgabe: 1,000.12 (US-Schreibweise)

Timeout / Interval

Die Basis-Adapter-Implementierung erlaubt das verwalten von Timeouts und Intervallen. Das nutzen dieser Adapter-Funktionen stellt sicher, dass alle Timeouts und Intervalle beim Stop der Instanz korrekt abgebrochen/beendet werden.

Die Signaturen der Funktionen sind dabei identisch zum JavaScript-Standard.

const id = this.setTimeout(callback, timeout, ...args);
this.clearTimeout(id);

const id = this.setInterval(callback, timeout, ...args);
this.clearInterval(id);

Instanz stoppen

Soll eine Adapter-Instanz außerplanmäßig gestoppt werden (weil z.B. die Konfiguration fehlerhaft ist), passiert dies über terminate() oder process.exit(). Hier muss unbedingt geprüft werden, ob die Terminate-Funktion existiert!

Im Compact Mode laufen alle Instanzen im gleichen Prozess - daher wäre es sehr schlecht, wenn der komplette Prozess beendet würde!

Als Exit-Code kan aus den Adapter-Utils ein Grund gewählt werden.

if (typeof this.terminate === 'function') {
    this.terminate(utils.EXIT_CODES.INVALID_ADAPTER_CONFIG);
} else {
    process.exit(utils.EXIT_CODES.INVALID_ADAPTER_CONFIG);
}

Wenn ein Adapter vom Typ schedule seine Arbeit erledigt hat, muss dieser den eigenen Prozess selbst beenden. Ansonsten würde beim nächsten Start (nach Zeitplan) gemeldet, dass die Instanz bereits läuft:

if (typeof this.stop === 'function') {
    this.stop();
}

Lizenzen

Unterstützt seit js-controller Version 4.0.15

Alle gültigen Lizenzen für einen Benutzer werden im Objekt system.licenses abgelegt (intern Lizenz-Manager genannt). Gültige Lizenzen für den aktuellen Adapter können wie folgt abgefragt werden:

await this.getSuitableLicenses(all?: boolean, adapterName?: string);

Dabei wird der folgende Public Key des js-controller verwendet, um die Lizenz zu verifizieren (der Private Key liegt wahrscheinlich auf irgend einem Cloud-Server um die Lizenzen zu erstellen):

/opt/iobroker/node_modules/@iobroker/js-controller-adapter/build/cert/cloudCert.crt

Die Lizenzen werden als JWT (JSON Web Token) abgelegt.