NEWS
Ext. NodeJS Module in Node-Red on the fly nutzen (ab V 3.0)
-
Ab dem Node-Red Adapter 3.0 beinhaltet dieser eine Node-Red Version höher als 2.x, die es sehr einfach macht externe NodeJS Module zu nutzen. Allerdings sollte man doch einiges beachten, um sein System hier nicht "kaputt" zu installieren.
Da ich im Hauptthread zum Node-Red Adapter 3.0.0 angekündigt hatte, ein paar Beispiele vorzustellen, will ich das hier in einem gesonderten Thread machen, um den bestehenden Thread nicht zu überfrachten.
Im Hauptthread hatte ich ja folgendes geschrieben:
Eines der gewaltigsten Vorteile der NR Version 2 ist die Installation und das Nutzen externer NodeJS Module in Function Nodes ohne diese installieren zu müssen!!!
Die Module müssen also nicht in der settings.js bekannt gemacht werden und werden automatisch im
/opt/iobroker/iobroker-data/node-red/node_modules Verzeichnis installiert. Es ist KEINE Installation über Adaptereinstellungen oder über die Kommandozeile erforderlich.
Die Module können einfach in den function Nodes über den Setup-Tab bekannt gemacht werden:
Ein Beispiel gibts ggf. noch in einem der Anschlussposting.
Und um diese Beispiele soll es nun in diesem Thread gehen.
Zuerst jedoch einmal ein paar grundsätzliche Bemerkungen, die zu beachten sind, dass man sich eben nicht lauter Mist in sein System holt und sich wundert oder sogar Konflikte entstehen. Außerdem sollte man sich beim Einsatz externer Module im Klaren sein, dass man damit sein gesamtes NodeRed System "abschiessen" kann - das habe ich nun selbst erlebt.
Alle Module die "on the fly" installiert werden, müssen manuell wieder deinstalliert werden!
Deshalb sollte man sich ggf. mal seine node_module Verzeichnis anschauen, was da alles installiert wurde.
Ich habe mal 3 Beispiele vorbereitet, wie sich externe NodeJS Module sinnvoll nutzen lassen - und zwar mit Hilfe der neuen Funktion in den function Nodes.
1. Beispiel: - Information über YouTube Videos mit youtube-dl-exec (hier der Link: https://www.npmjs.com/package/youtube-dl-exec)
Anregung kam aus einem Thread eines Users, der diese Bibliothek zum Laufen bringen wollte - das ging auch, wenn auch die in dem Thread vorgestellte Lösung nicht mehr funktioniert und das System bei Fehleingaben zum Absturz bringt.
Anhand dieses ersten Beispiels gehe ich auf die grundsätzliche Vorgehensweise ein - auch wenn sich diese je nach Bibliothek ggf. ändern kann.
Installation:
Um eine externe Bibliothek in NodeRed in einer function Node zu nutzen - muss man diese nur noch im Setup Tab eingetragen werden - zusammen mit einer Variable wie das dann in der function Node referenziert wird.In dem Screenshot grün ist der Name der Bibliothek - die Variable rechts zeigt die Variable mit der ich diese Bibliothek anspreche.
Ob ich das direkt machen kann, ich einen Konstruktur verwenden muss und ein neues Objekt erstellen muss - kann man nicht allgemeingültig sagen, das hängt ja von der Bibliothek ab.
Sobald man diese function Node übernimmt/deployed - beginnt im Hintergrund schon die Installation. Man muss also erst mal nicht auf die Kommandozeile oder in irgend sonst einer Konfiguration das Modul bekannt machen. Man kann aber auch nicht erwarten, dass die function Node deshalb schon ein paar Sekunden nach dem Deploy funktioniert, sondern sollte dem Ganzen schon etwas Zeit lassen, je nachdem wie groß die Bibliothek ist.
Diese Setup Tab - entspricht im Wesentlichen im JS das require("NodeJS Name").
Man sieht dass alle benötigten Module im Hintergrund installiert wurden:
Wichtig ist auch die ganzen Module werden im NodeRed Datenverzeichnis installiert.
/opt/iobroker/iobroker-data/node-red/node_modulesDeinstallation
Nun greife ich gleich mal der Deinstallation vor - zum Glück muss man nicht jedes Modul einzeln deinstallieren, sondern auch hier werden die Abhängigkeiten erkannt.
Als erstes muss man natürlich alle function Nodes entfernen, die diese externen Bibliotheken nutzen. Es langt auch wenn man diese function Nodes deaktiviert. Sobald diese aber wieder aktiviert werden, beginnt die Installation im Hintergrund.
Um das sauber zu deinstallieren, macht man dies am Besten auf der Kommandozeile seines iobrokers.
Um keine Rechteprobleme zu bekommen, deinstalliert man die Module am Besten auch unter der Kennung des iobrokers, da sie ja mit dieser auch installiert wurden:
Man gibt also folgende Kommandos ein:
sudo -su iobroker cd /opt/iobroker/iobroker-data/node-red npm remove youtube-dl-exec
- Mit dem ersten Kommando - arbeitet man mit den folgenden Befehlen unter der Benutzerkennung des iobrokers.
- Man wechselt in das Datenverzeichnis des iobrokers und hier in node-red
- Dort entfernt man dann die installierte Bibliothek mit npm remove "Bibliotheksname"
Man sieht, dass alle Verzeichnisse wieder gelöscht und entfernt wurden. Das System ist also wieder sauber.
Sobald man ggf. vorhandene function Nodes, die diese Bibliothek wieder referenziert, aktiviert und deployed, erfolgt erneut die Installation. (Man achte auf die Zeitstempel der Verzeichnisse)
Betrieb
Nun bin ich nur auf Zufall durch diesen Thread auf die Bibliothek gestoßen und sie wurde in erster Linie dazu genutzt, um Informationen von YouTube Videos zu erhalten. (Clicks etc.).
Der ursprüngliche (wie in dem referenzierten Thread) Code hat nur soweit funktioniert, wenn die Eingabe des YouTube Videos korrekt ist.
Der Flow ist ziemlich einfach - man gibt als payload die URL des YouTube- Videos mit und erhält die entsprechenden Informationen.
Der Abruf arbeitet asynchron und als Promise-Objekt. Ich bin leider nicht so sattelfest in Javascript, aber soweit ich verstanden habe, gibt dieses Objekt quasi ein Signal, wenn es mit der asynchronen Verarbeitung fertig ist.
Wie man sieht habe ich in der ersten Inject Node ein fehlerhaften Aufruf (nicht existierende URL) eingegeben.
Mit dem ursprünglichen Code ist Node-Red komplett abgestürzt:
const myTimeout = setTimeout(NoReturn, 10000); youtubeDlExec(msg.payload, {dumpSingleJson: true}).then(output => { msg.payload = output; clearTimeout(myTimeout); node.send(msg); node.done(); }); function NoReturn(){ msg.payload = "No response"; node.send(msg); node.done(); }
wobei dieser Code noch vor ein paar Wochen funktioniert hat. Bei einem fehlerhaften Aufruf, hat einfach ein Timer zugeschlagen, der dann zurückgegeben hat, dass keine Rückmeldung kam. Die asynchrone Verarbeitung sieht man einfach daran, dass das Nachrichtenobjekt nicht mit return (return msg) zurückgegeben wird, sondern über node.send(msg). Die Abarbeitung der function Node ist also schon längst fertig, während im Hintergrund noch die Informationen zu dem Video abgerufen werden.
Das geht heute nicht mehr - sondern dieser Code erzeugt einen kompletten Absturz von Node-Red!!!!
Im iobroker - wird aber die Ursache geloggt - so schaut dann so ein Komplettabsturz aus:
2022-03-16 11:29:11.304 - error: node-red.0 (19800) Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). 2022-03-16 11:29:11.314 - error: node-red.0 (19800) unhandled promise rejection: Command failed with exit code 1: /opt/iobroker/iobroker-data/node-red/node_modules/youtube-dl-exec/bin/youtube-dl https://www.youtube.com/watch?v=Err6xKWiCMKKJg --dump-single-json ERROR: Video unavailable 2022-03-16 11:29:11.315 - error: node-red.0 (19800) Error: Command failed with exit code 1: /opt/iobroker/iobroker-data/node-red/node_modules/youtube-dl-exec/bin/youtube-dl https://www.youtube.com/watch?v=Err6xKWiCMKKJg --dump-single-json ERROR: Video unavailable at makeError (/opt/iobroker/iobroker-data/node-red/node_modules/execa/lib/error.js:60:11) at handlePromise (/opt/iobroker/iobroker-data/node-red/node_modules/execa/index.js:118:26) at runMicrotasks () at processTicksAndRejections (internal/process/task_queues.js:95:5) 2022-03-16 11:29:11.316 - error: node-red.0 (19800) Command failed with exit code 1: /opt/iobroker/iobroker-data/node-red/node_modules/youtube-dl-exec/bin/youtube-dl https://www.youtube.com/watch?v=Err6xKWiCMKKJg --dump-single-json ERROR: Video unavailable 2022-03-16 11:29:12.005 - info: node-red.0 (19800) terminating 2022-03-16 11:29:12.007 - warn: node-red.0 (19800) Terminated (UNCAUGHT_EXCEPTION): Without reason 2022-03-16 11:29:13.132 - info: node-red.0 (19800) terminating with timeout 2022-03-16 11:29:14.052 - warn: node-red.0 (19800) get state error: Connection is closed. 2022-03-16 11:29:14.054 - warn: node-red.0 (19800) get state error: Connection is closed. 2022-03-16 11:29:14.065 - warn: node-red.0 (19800) get state error: Connection is closed. 2022-03-16 11:29:14.149 - warn: node-red.0 (19800) Could not perform strict object check of state 0_userdata.0.stromverbrauch.shellies.shellyplug-s-7AE344_0.power: DB closed 2022-03-16 11:29:14.150 - warn: node-red.0 (19800) get state error: Connection is closed. 2022-03-16 11:29:14.970 - info: node-red.0 (19770) node-red exited with 6 2022-03-16 11:29:19.972 - info: node-red.0 (19770) Starting node-red: --max-old-space-size=512 /opt/iobroker/node_modules/iobroker.node-red/node_modules/node-red/red.js -v --settings /opt/iobroker/iobroker-data/node-red/settings.js 2022-03-16 11:29:36.406 - warn: node-red.0 (25737) slow connection to states DB. Still waiting ...
Und im Node-Red Forum hat mir dann der NodeRed Guru geholfen, wie man sowas abfängt. Das ist also insbesondere wichtig bei promise Objekten und asynchroner Verarbeitung.
Um also solche Abstürze zu vermeiden, kann man diesen Promise Objekten - direkt ein catch Objekt anhängen!!!
Der Code der also fehlertolerant ist, sieht nun so aus:
youtubeDlExec(msg.payload, {dumpSingleJson: true}).then(output => { msg.payload = output; node.send(msg); node.done(); }).catch(err => { // An error has been returned by the Promise. // Do something with it... msg.payload = {name: err.name, message:err.message}; node.send(msg); node.done(); });
Wichtig ist also direkt nach dem Aufruf ein
.catch(err =>{})
anzuschließen.
Der Fehler wird nun sauber abgefangen und innerhalb von NodeRed ausgegeben:
Damit wäre mein 1. Beispiel fertig.
2. Beispiel : Das 2. Beispiel ist in meinen Augen eine ganz pfiffige Bibliothek mit der man JSON Dateien, wie eine kleine Datenbank behandeln kann.
Normalerweise speichern wir ja unsere Daten, die auch einen Stromausfall überleben sollen in mqtt oder eben im iobroker. Wer jedoch gerne auch mal seine eigenen Konfigurationsdateien außerhalb irgendwelcher Systeme in reinen JSON Dateien speichern möchte, für den gibt es hier eine pfiffige NodeJS Lösung, die sich node-json-db nennt.
Hier die Beschreibung mit den Möglichkeiten: https://www.npmjs.com/package/node-json-db
Installation:
Dieses Mal benötigt man 2 Zugänge zu einer Bibliothek (wie man aus dem JS Code unter npm) erkennen kann.
Einmal für die Bibliothek selbst und einmal für die Konfiguration der Datenbank bzw. JSON Datei.
Die Anzahl der installierten Module und Abhängigkeiten ist dieses Mal gering:
Deinstallation:
hier gehe ich nicht mehr im Detail darauf ein - ist wie beim 1. Beispiel
Betrieb
Den Code in der function Node habe ich auch versucht sehr übersichtlich zu gestalten:
const JsonDB = nodeJsonDb.JsonDB; const Config = nodeJsonDbDistLibJsonDBConfig.Config; var db = new JsonDB(new Config("/home/iobroker/mwDataBase", true, true, '/')); msg.topic = msg.topic || '/'; if (msg.payload) db.push(msg.topic, msg.payload,false); if (msg.delete) db.delete(msg.delete); msg.payload = db.getData(msg.topic); return msg
Die ersten beiden Zeilen erstellen die benötigten Objekte, um dann über einen Konstruktur die Datenbank - sprich die JSON Datei als neues Objekt zu erzeugen.
Die Datei habe ich in das Homeverzeichnis des iobrokers gelegt, da dieser darauf in jedem Fall Schreibrechte hat.Die Datenbank heißt also mwDataBase.json
Der Code ist wie gesagt sehr einfach gehalten:
- Wird nur ein msg.topic mitgegeben, dann wird dieses topic aus der Datenbank ausgelesen und in der msg.payload ausgegeben.
- Wird ein msg.topic und ein msg.payload mit gegeben, wird die msg.payload an die Stelle des msg.topics in der Datei geschrieben und die msg. payload ausgegeben.
- Wird ein topic in einer msg.delete Eigenschaft mitgegeben, so wird dieses topic gelöscht.
Das Teil kann zwar noch mehr, aber für Demonstationszwecke reichen diese Tasks.
Als Anwendungsfälle habe ich nun 2 Konfigurationsdatenblöcke, die mir
- bei meinem TV (Sky-Q) - zu dem Sendernamen, die Favoritennummer ausspuken soll
und - einem Shelly - Bezeichnung in einen benutzerfreundlichen Namen übersetzen soll (ich weiß, kann man im Shelly selbst konfigurieren).
Das Schreiben in die Datenbank schaut also mittels einer Inject Node so aus:
das Schreiben in einen iobroker-Out Datenpunkt - spare ich mir.
Die JSON Datei zu den Sendernamen, sieht wie folgt aus:
Um nun den Favoriten- bzw. Sendeplatz vom z.Bsp des ZDF zu ermitteln gebe ich in einer payload ZDF mit (das gesamte JSON Objekt, das ich normalerweise aus dem iobroker DP auslesen würde, befindet sich in msg.sender).
Über die JSON-DB Bibliothek gebe ich einfach den Pfad zum ZDF mit, um den Senderplatz zu ermitteln:
Der 2. Anwendungsfall ist nun, dass mir die JSON String einen benutzerfreundlichen Namen für einen Shelly ausgeben soll.
Außerdem habe ich als Beispiel einen Shelly 2.5 mit 2 Relais ausgesucht.In der Datenbank schaut die Struktur nun so aus:
Über den Relais Index möchte ich nun den entsprechenden Namen herausbekommen.
Mitgegeben wird nur der Name und der Index. Hier zu Demozwecken natürlich das gesamte Objekt (friendlyNames), was man in der Regel sonst aus dem iobroker ausliest:
Hier war es schon etwas mehr tricky den richtigen JSONATA Ausdruck zu finden.
funktioniert aber wie folgt:
Hier mal wieder als kleiner Flow:
Über die Bibliothek muss man das Topic nur als String zusammensetzen, wie aus dem folgenden Bild zu erkennen:
3. Beispiel: Das 3. und letzte Beispiel in diesem Thread bindet nur eine kleine Library ein, um den Taupunkt oder Hitzeprobleme zu berechnen. Natürlich kann man das auch manuell berechnen, aber so wie ich es aus einem Beispiel mal rauskopiert habe, kommen teilweise falsche Werte raus und man sollte diese Ausgangswerte auch in einem Objekt mitführen.
Sowohl Hitzewarnungen und Taupunkt benötigen Temperatur und Luftfeuchtigkeit.
Die Bibliotheken sowie die zugrundeliegenden Berechnungen findet man hier:
Beispiel der Einbindung externer Node.JS Bibliotheken
@tsmx/weather-tools von https://github.com/tsmx/weather-tools#dew-point-functions
Hier die verwendeten Berechnungen:
Hitzindex: https://de.wikipedia.org/wiki/Hitzeindex
Taupunkt: https://de.wikipedia.org/wiki/TaupunktInstallation:
Dieses Mal ist es wirklich nur eine Bibliothek, die installiert wird:
Deinstallation:
siehe 1. Beispiel
Zur Deinstallation diesen Befehl eingeben:
npm remove @tsmx/weather-tools
Betrieb:
Dieses Mal poste ich den ganzen Flow. Wer ihn importiert, installiert somit auch die @tsmx Bibliothek:
Das erste ist eine manuelle Berechnung für den Taupunkt (die ich irgendwoher mal geklaut habe) - wobei die zwar meist stimmt, manchmal kommen auch falsche Werte raus - das Problem ist, dass hier auf 2 Nachrichten gewartet wird, aber trotzdem manchmal was ausgegeben wird.
Diese relativ komplexe Berechnung des Taupunktes:
var tempf = context.get('tempf')|| 0; var humidity = context.get('humidity')|| 0; var oldTp = context.get('contexttp') || 0; var tp = 0; if (msg.topic === "tempf") { tempf= msg.payload; context.set('tempf',msg.payload); } else if (msg.topic === "humidity") { humidity= msg.payload; context.set('humidity',msg.payload); } // Quelle: https://www.chemie-schule.de/KnowHow/Taupunkt // Gültigkeitsbereich Taupunkt: -30°C <= tmp <= 70°C if (!(isNaN(tempf) || isNaN(humidity))) { tp = ((241.2 * Math.log(humidity/100)) + ((4222.03716 * tempf) / (241.2 + tempf))) / (17.5043 - Math.log(humidity/100) - ((17.5043*tempf)/(241.2+tempf))); tp = Math.round( tp * 100 ) / 100; context.set("contexttp",tp); } if (oldTp !== tp) { msg.payload = tp; msg.topic = "Taupunkt"; return msg } else { return null; }
reduziert sich mit der Library auf einen Einzeiler:
msgNew.payload.tauPunkt = wt.dewPoint(msg.payload.temperature, msg.payload.humidity)
Wichtig ist halt, dass Temperatur und Luftfeuchte in einem mitgegeben werden.
So ich hoffe es macht Spaß, das ggf. nachzuvollziehen und gibt vielleicht für den einen oder anderen ein paar Hinweise zur Umsetzung. Ich lerne ja auch immer noch dazu.