NEWS
Test: Objektvorlagen und Hilfsfunktionen für Objektbäume
-
Hallo Devs!
Heute möchten @AggroRalf und ich unser neuestes Projekt vorstellen. Die Idee basiert auf den Objekt-Vorlagen aus https://github.com/iobroker-community-adapters/iobroker-adapter-helpers/, erweitert diese aber um ein paar Features:
- Typdefinitionen, die das "Gehirn" hinter der Syntax-Hilfe und Auto-Vervollständigung sind, und so die Nutzung vereinfachen.
- Funktionen zum Erstellen von Objekten aus Vorlagen (z.B. für json, Batterie, Licht, ...) oder mit selbst-definierten Rollen
- Helfer-Funktionen zum Verwalten und Synchronisieren des Objektbaums mit einem "Soll"-Zustand, inklusive Optionen zum Überschreiben von existierenden und Löschen von nicht mehr benötigten Objekten/States.
- Grundlegende Validierung von Objektdefinitionen
Ziel dieses Projekts ist es, die Adapter-Entwicklung für Adapter zu beschleunigen, die externe APIs einbinden, und deren Strukturen in ioBroker abbilden wollen. Außerdem soll durch die vordefinierten Strukturen vermieden werden, dass jeder seine eigenen Definitionen bauen muss und möglicherweise Vorgaben nicht beachtet.
Der Workflow in Adaptern funktioniert dann wie folgt:
- Daten aus API lesen
- Liste an benötigten Objekten und Werten erzeugen
- Diese an die Funktion
syncObjects
übergeben, fertig!
Das Projekt und eine Anleitung findet ihr unter https://github.com/gaudes/iobroker-object-helper.
Wir freuen uns auf eure Tests und vor allem Feedback!ACHTUNG: Bitte noch nicht produktiv nutzen. Im ersten Schritt wollen wir Feedback sammeln, um es später im bestmöglichen Zustand in
@iobroker/adapter-core
zu integrieren. Bis dahin behalten wir uns vor, die API-Schnittstelle zu ändern. -
[reserviert]
-
Huhu,
coole Idee und geht bissl in die Richtung die ich gebaut habe für zwei Meiner Adapter.
https://github.com/Apollon77/iobroker-tools/Ich habe zwei Dinge drin die ich euch bitten würde zu berücksichtigen:
1.) Ein Learning war das User teilweise die Namen von Objekten (meist Devices/Channels) gern mal ändern wollen und es blöd ist wenn die beim nächsten Adapterstart wieder überschrieben werden. Daher habe ich eine Möglichkeit bestimmte Felder beim "Objekt gibts schon - updaten" Fall zu erhalten
2.) Ich find es bei ioBroker nervig das die "stateChange" Handler an einer ganz nderen Stelle im Code stehen ls wo man die Objekte definiert. Da wo die Objekte definiert werden hat man üblicherweise alle Daten verfügbar die man so braucht und muss Sie sich im stateChange Handler neu besorgen oder irgendwie speichern oder so. Daher habe ich das Feature das ich den "stateChange Handler" (Callback function) direkt beim objekt anlegen mitgeben kann. Das onStateChange hat dann nur eine Methode die das intern wieder mappt und die korrekte Methode ausführt. gleiches könnte man für objectChange machen. https://github.com/Apollon77/iobroker-tools/blob/master/lib/objectHelper.js#L247
Was ich noch tue ist generell bissl konsistenzkram zu machen https://github.com/Apollon77/iobroker-tools/blob/master/lib/objectHelper.js#L17
Das Feature alle objekte aufzuräumen habt Ihr ja schon drin.
Ingo F
-
@apollon77 sagte in Test: Objektvorlagen und Hilfsfunktionen für Objektbäume:
die ich gebaut habe für zwei Meiner Adapter
Und da haben wir schon einen Grund, warum wir das gemacht haben
-
Danke für das super Projekt! Ich habe mir die aktuelle "API" angeschaut und überlegt, wie man den Wunsch von @apollon77 und weitere Ideen sinnvoll zur Verfügung stellen könnte.
Ich schreibe hier einfach mal auf, wie ich mir die API als "Benutzer" aus TypeScript vorstelle (auf Basis eurer Beispiele und der gewünschten Erweiterungen):
import { IobObjectHelper } from "iobroker-object-helper"; // create helper const helper = new IobObjectHelper(this /* adapter instance */); // create channel const userChan = helper.addChannel("user" /* id */, { name: "user" }); // create states const jsonState = userChan.addStateFromTemplate("json" /* state id without parent */, "json" /* template name */, { name: "json", value: JSON.stringify(yourjsondata) }); const idState = userChan.addState("id", { name: "userid", value: yournumericuserid, role: "value", description: "Numeric User-ID" })); // subscribe to state changes idState.on('change', { change: 'ne' }, state => this.log.info(`State ${state.id} changed to ${state.val}`)); // there is also an off() method to remove the change handler // subscribe to object changes idState.on('object-change', { /* ??? */ } , obj => this.log.info(`Object of state ${obj.id} changed`)); await helper.syncObjectsAsync({ removeUnused: true, except: /info.*/ }); // update values after the objects were synchronized idState.setValue(355 /* value */, { ack: true } /* options */); // or the same with waiting for the state to be changed: await idState.setValueAsync(355 /* value */, { ack: true } /* options */);
Damit (und wohl mit weiteren Methoden auf den zurück gegebenen Objekten) könnte man das gesamte State Handling massiv vereinfachen. Dann kann nämlich die Stelle (z.B. Klasse) seinen State als Variable haben und muss sich nicht mehr um IDs kümmern (nach dem erstellen).
Der Grund für spezifische "addXxx" Methoden ist, dass man schon vom Methodennamen auf den zurückgegebenen Typen schliessen kann, denn ein
addStateXxx()
wird ein Objekt zurück geben, das kein weiteresaddXxx()
zulässt, während einaddChannelXxx()
ein Objekt zurück gibt, auf dem es keinsetValue()
gibt.Ob man zwingend immer mit Parent/Child arbeiten muss, ist zu diskutieren; ich sehe für beides Vor- und Nachteile. Es könnte also immer noch möglich sein, das folgende zu schreiben:
const idState = helper.addState("user.id", { name: "userid", value: yournumericuserid, role: "value", description: "Numeric User-ID" }));
Ich bin gespannt auf euer Feedback.
-
Ich bin mir gerade nicht sicher ob ein eigenes Objekt pro State zu haben wirklich Dinge so arg vereinfacht ... aber ja wäre so möglich.
Die andere Variante (also States mit ". getrennten Pfaden) frei zu nutzen wäre dennoch weiter sinnvoll, weil sonst eine pot. Migration von Bestandsadaptern um einiges aufwändiger
-
Schön, dass sich hier nun eine Diskussion entwickelt
@UncleSam: Zum Thema "Klassen" sollte besser @AlCalzone was sagen, ich persönlich liebe Klassen
Während ich über den Text hier überlege, fällt mir aber doch noch was ein: Ich mache ja auch etwas C# beruflich. Grad bei Klassen und dahinterliegenden "Datenbanken" denkt man ja eher an eine direkte Schnittstelle zur DB, so wie Du vermutlich bei deiner Idee.
Die Idee mit dem dynamischen Objektbaum in der Klasse finde ich etwas zu komplex. Klassen sind halt in der Regel recht fix definiert, z.B. Klasse "Mitarbeiter", "Aufträge" oder so. Dann müsste ich als Entwickler immer überlegen und selbst den Baum vor Augen haben.
Mit dem bisherigen Vorschlag schreibe ich meine Objekte und beim Speichern bekomme ich ggf. einen Fehler wenn z.B. ein Folder fehlt. Finde das auch einfacher.
Also obwohl ich Klassen ja sehr mag, ist es meines Erachtens für die Entwickler einfacher und übersichtlicher hierfür keine Klasse zu verwenden. Sonst würde wir ja über die Klasse quasi einen komplett getrennten, dafür mehr objektorientierten Zugang zu den Objekten und States aus ioBroker aufbauen. Das wäre ja dann auch keine "einfache Hilfsfunktion für Objektbäume" mehr, sondern eine fast vollständige objektorientierte Schnittstelle zu den Objekten und States, analog wie man es z.B. in anderen Programmiersprachen mit Datenbanken und Klassen realisiert.
Die Idee vereinfachte Funktionsaufrufe verfügbar zu machen finde ich überlegenswert. Vorteil wäre, man sieht schneller im Code was man da grad macht. Der Aufruf hierfür wäre z.B. für Channel oder Folder auch simpler. Auf der anderen Seite hast dann ggf. viele Aufrufe mit unterschiedlichen Parametern, dass macht es nicht einfacher bzgl. Lernkurve.
Was mir bei deinem Ansatz aber grundsätzlich sehr gefällt ist die bessere Lesbarkeit. Bin da grad so hin und her gerissen ehrlich gesagt.
Aktuell musst eine Funktion anschauen und deren Parameter. Damit kannst es befüllen. Dann noch eine weitere Funktion zum Speichern. Fertig. Auch wenn wir öfters "this" übergeben und es sich nicht schon durch den Namen der Funktion erklärt was dort passiert.
Ich habe immer noch die Warnung von @apollon77 bei der ursprünglichen Diskussion in Discord als stetige Mahnung im Kopf: Nicht zu kompliziert, keine "Eierlegende Wollmilchsau", daran sind schon andere gescheitert
Daher sehe ich es auch bei der "onChange"-Sache wie @AlCalzone : Der Helper soll es für einfache Dinge noch einfacher machen, der Hintergrund waren ja auch z.B. einfache API-Adapter. Die unendlichen Weiten der komplexen Sonderfälle soll und kann der Helper vermutlich nicht abdecken.
-
@AggroRalf Ja, ich komme auch von C# (man merkt's ).
TypeScript erlaubt leider auch etwas wahnsinniges Verhalten mit Methodenparametern. Ich bevorzuge Methodennamen, die mir schon helfen, was ich machen soll und nicht unbedingt die TypeScript Variante von "wenn das erste Argument x ist, dann kann das zweite Argument nur noch a oder b sein und wenn du dann b im zweiten Argument hast, gibt die Methode Z zurück". Ich weiss es ist machbar (und absolut cool), aber verständlicher sind einfach Methodensignaturen, die von Code Completion unterstützt werden und nicht eine Methodensignatur die nur mit Code Completion verstanden werden kann.
Zur Verteidigung meines Ansatzes: wer nicht will, muss die zurückgegebenen Objekte nicht beachten und kann (wie @apollon77 vorgeschlagen hat) fast gleich arbeiten wie bis jetzt. Der Vorteil mit den zurückgegebenen Objekten ist, dass wir darauf eine "neue" API aufbauen können - über Zeit und Schritt für Schritt.
Schlussendlich schreiben wir in etwas komplexeren Adaptern immer wieder denselben Code und haben häufig "komische" Abhängigkeiten (jedes Objekt muss den Adapter kennen), die wir so vereinfachen könnten. Benutzen müssen es ja nur die, die wollen.
Gerade wenn wir den "Helper" in die adapter-core einbauen wollen (was super wäre), würde es IMHO eben Sinn machen, dass wir eine gute API zur Verfügung stellen (die auch "vorwärtskompatibel" ist - sprich: erweitert werden kann). Da wurde in der Vergangenheit oft einfach etwas rumgebastelt.
-
@AggroRalf sagte in Test: Objektvorlagen und Hilfsfunktionen für Objektbäume:
Zum Thema "Klassen" sollte besser @AlCalzone was sagen, ich persönlich liebe Klassen
Ich habe erst mal nichts gegen Klassen, sehe sie von .NET-Entwicklern, die mit JS anfangen, aber gerne überbenutzt.
Bei unserem ursprünglichen Design fand ich es Overkill, alles in Klassen zu kapseln, die nachher nur Daten enthalten. Für die vorgeschlagene API, insbesondere mit den lokalen Change Handlern macht es durchaus Sinn.@UncleSam sagte in Test: Objektvorlagen und Hilfsfunktionen für Objektbäume:
wahnsinniges Verhalten mit Methodenparametern. Ich bevorzuge Methodennamen, die mir schon helfen, was ich machen soll und nicht unbedingt die TypeScript Variante von "wenn das erste Argument x ist, dann kann das zweite Argument nur noch a oder b sein und wenn du dann b im zweiten Argument hast, gibt die Methode Z zurück"
Daher sind wir beim Modell "Bag of Options" gelandet. Du übergibst ein Objekt (was dann zwar recht komplex werden kann, was die möglichen Zusammenhänge betrifft), aber jede Eigenschaft wird auch an sich verstanden, weil sie einen Namen hat. Für ein ausführliches Beispiel siehe folgende PR-Beschreibung: https://github.com/gaudes/iobroker-object-helper/pull/4
Kurz:buildObject({ id: "bla", name: "blub", description: "abc", value: 1, objectType: "info", role: "switch", /* oops, this one is too much: */ "bla" }); // statt buildObject("bla", "blub", "abc", 1, "info", "switch", "bla"); // ???
Meiner Ansicht nach können wir versuchen, beide Welten zu verbinden. Für manche macht es sicher Sinn, wie derzeit implementiert eine Liste von Objekten dynamisch zu erstellen. Da bei jedem zu prüfen, welches Ober-Objekt genutzt werden muss, macht es unnötig kompliziert.
Bei klarer definierten Strukturen sehe ich den Vorschlag durchaus als Vorteil an. -
@AlCalzone Danke für dein differenziertes Feedback!
Daher sind wir beim Modell "Bag of Options" gelandet.
Bag of Options sehe ich als gute Lösung für genau das: Optionen. Zwingende Argumente bevorzuge ich als Parameter, aber ich weiss, dass das bei vielen JavaScript APIs anders gemacht wird - oder zumindest beides angeboten wird. Ich bin auch ganz klar gegen Methoden Overloads, da die in JavaScript keinen und in TypeScript nur bedingt Sinn machen.
Wenn du meinen Vorschlag anschaust, sollte das genau dem entsprechen: jede Methode hat ein paar zwingende Argumente und dann einen Bag of Options. Es gibt keine Overloads. Verschiedene Funktionalitäten werden durch unterschiedlich benannte Methoden zur Verfügung gestellt. Und etwas, was ich bei C# gelernt habe: async Methoden heissen xxxAsync - das vermindert das Risiko von floating Promises.
-
@UncleSam sagte in Test: Objektvorlagen und Hilfsfunktionen für Objektbäume:
Und etwas, was ich bei C# gelernt habe: async Methoden heissen xxxAsync - das vermindert das Risiko von floating Promises.
Wenn ich das so machen würde, würden 90% meiner Funktionen xxxAsync heißen
-
@UncleSam sagte in Test: Objektvorlagen und Hilfsfunktionen für Objektbäume:
Gerade wenn wir den "Helper" in die adapter-core einbauen wollen (was super wäre), würde es IMHO eben Sinn machen, dass wir eine gute API zur Verfügung stellen (die auch "vorwärtskompatibel" ist - sprich: erweitert werden kann). Da wurde in der Vergangenheit oft einfach etwas rumgebastelt.
Adapter-Core? Extra Library? Direkt in den controller?
-
@apollon77 Mein Plan wäre adapter-core. Direkt mit dem Controller hat das IMO nichts zu tun - es ist schließlich eine Abstraktion zur Hilfe. Eine weitere Library, die dann jeder braucht, ist aber auch wieder blöd.
-
@AlCalzone jeder braucht? jeder der die convenient methoden nutzen will weil SIe ja "On top of adapter.js" existieren ... Vorteil von eigener lib wäre flexiblere versionierung ... Adapter Core hat auch immer eine js-controller dependency (2.3.1 braucht controller 1.5.8+, 2.4.0 brauche js.controller 2.0+)
Ich hab Angst das es zu undurchsichtig wird wenn adapter-core jetzt noch mehr "features" bekommt und damit aktiver neue Versionen ....
-
Also nochmal zur Grundidee: Wir wollten ja die Entwicklung vereinfachen, z.B. gerade für simple API-Adapter.
Ich denke, dass wir das Ziel mit dem aktuellen Entwurf erreicht haben. Es sind einfach zwei Funktionen, mehr nicht. Klar haben die ein paar Parameter, aber das kriegt auch ein Anfänger per Cut+Paste hin. Durch die Vorlagen erreichen außerdem wir eine bessere Validität der Objekte.
@UncleSam 's Idee finde ich als "Ein-bischen-C#-Entwickler" auch sehr elegant, denke aber das würde ja das ganze Modell verändern. Es wäre eine komplett neue, objektorientierte Schnittstelle. Hätte diese Lösung für einfache API-Adapter einen Vorteil hinsichtlich Einfachheit und schnellere Entwicklung ?
@apollon77 Das mit dem Core war ja die Idee, da es ja (aktuell) nur zwei "einfache" Funktionen sind. Diese wären dann einfach vorhanden, gerade auch für Einsteiger bei der Entwicklung. Wenn man wieder eine Abhängigkeit einbauen muss, diese dann noch ggf. updaten muss, .... dann schreckt das doch eher ab für den Ein- und Umsteiger. So wäre es direkt vorhanden und man könnte es nutzen.
-
@AggroRalf sagte in Test: Objektvorlagen und Hilfsfunktionen für Objektbäume:
Hätte diese Lösung für einfache API-Adapter einen Vorteil hinsichtlich Einfachheit und schnellere Entwicklung ?
Für Adapter, die nicht nur Werte erhalten macht das schon Sinn. Meine drei Adapter (Loxone, i2c und Luxtronik2) bräuchten alle dasselbe. Nun kann ich auch noch einen Helper schreiben, oder @apollon77 's Modul nehmen oder wir bauen es einfach gleich mit eurer Lösung zusammen ein.
Die Idee ist ja: nicht 500 Lösungen für dasselbe Problem zu haben. Und für die ganz einfachen Adapter spielen wie gesagt die zurück gegebenen Objekte keine Rolle. Wer die nicht verwenden will, muss sie nicht verwenden. Für alle anderen (mit @apollon77 zusammen sind das ja schon fünf!) ist es eine massive Vereinfachung.