72_UBUS.pm Code Review / Feedback

Begonnen von xenos1984, 08 August 2021, 17:47:27

Vorheriges Thema - Nächstes Thema

xenos1984

Danke, Rudi, für die Erläuterungen! Das entspricht so weit auch meinem Verständnis aus der Doku im Wiki.

Bei den WebSocket und HTTP Verbindungen, die jeweils asynchron kommunizieren, erscheint mir der Ablauf halbwegs klar. Da geschieht die Zuordnung zwischen Anfrage und Antwort über eine ID, die bei beiden Richtungen identisch ist. Wenn also ein logisches Gerät eine Anfrage mittels IOWrite schickt, ist dieser Anfrage eine ID zugeordnet. Wenn eine Antwort kommt, muss Parse nur schauen, welches Gerät die Anfrage dieser ID geschickt hat. Im Prinzip sehe ich dann zwei Möglichkeiten einer Implementierung:

  • Wenn ich das richtig sehe, gibt IOWrite auch den Rückgabewert der Write-Funktion des physikalischen Moduls an das logische Modul zurück. Man könnte also die Erzeugung und Verwaltung der IDs dem physikalischen Modul überlassen. Write erzeugt eine ID und führt den Aufruf aus, gibt dann die ID an das logische Modul zurück, und letzteres merkt sie sich. Wenn die Antwort kommt, ist die ID darin enthalten, und das logische Modul prüft in Parse, wer die Anfrage gestellt hat. "Bitte stellen Sie eine Frage und ziehen Sie eine Nummer. Sie werden aufgerufen." 8)
  • Andererseits übergibt IOWrite alle seine Argumente direkt an Write. Also könnte auch das logische Modul eine ID erstellen und zusammen mit der Anfrage übergeben. Die ID kann eine Zeichenkette sein, könnte also einfach den Namen des Geräts + eindeutiger Kennung enthalten. "Bitte stellen Sie eine Frage und geben Sie uns Ihre Telefonnummer und das Aktenzeichen, wir rufen zurück." 8)
Noch bin ich etwas unschlüssig, was besser ist, da es immer noch den Sonderfall gibt, der keine ID benutzt. Wenn man UBUS nicht über eine Schnittstelle, sondern über die Kommandozeile benutzt, erfolgt der Aufruf synchron, und es gibt keine ID. Letzteres ist vermutlich das kleinere Problem, da das physikalische Modul ja bei einem synchronen Aufruf die Zuordnung zwischen Anfrage und Antwort gegeben hat, und den ID-Mechanismus für das logische Modul simulieren kann.

Etwas unklar ist mir noch, wie man in dem Fall am besten die Antwort zurück schickt. Die kommt ja in dem Fall nicht über Read. Und in Write synchron Dispatch aufzurufen, ist vermutlich keine gute Idee, wenn das logische Modul als Reaktion gleich eine weitere Anfrage mittels IOWrite schicken könnte... Das gäbe eine Endlosschleife. Außerdem würde das nicht mit dem Mechanismus 1 von oben funktionieren, weil das logische Modul die Antwort bekäme, bevor das physikalische Modul über den Rückgabewert die ID mitgeteilt hat. D.h. die Nummer würde aufgerufen, bevor sie gezogen wurde :D Mechanismus 2 ginge, wenn sich das logische Modul die ID merkt, bevor es IOWrite aufruft.

rudolfkoenig

Version 2 ist mir sympatischer, da die Abhaengigkeit zwischen den Modulen hier etwas kleiner ist.
Das Problem mit dem Antwort zurueckschicken habe ich leider nicht wirklich verstanden.

xenos1984

Zitat von: rudolfkoenig am 20 September 2021, 11:28:21
Version 2 ist mir sympatischer, da die Abhaengigkeit zwischen den Modulen hier etwas kleiner ist.
Inzwischen habe ich mir die Abfragen weiter angesehen und ich denke, dass beide Vor- und Nachteile haben, und man vielleicht die Vorteile von beiden nutzen kann. Letztlich soll die ID ja zwei Dinge erfüllen: A. Zuordnung einer Antwort zu dem logischen Device, das die Anfrage gestellt hat (mittels Dispatch / Parse), B. eindeutige Zuordnung der Antwort zu einer Anfrage innerhalb des logischen Device. Um A zu erreichen, macht es Sinn, den Namen des Device als Teil der ID zu benutzen, um diesen dann in Parse abzufragen. Dafür bräuchte es Methode 2 aus dem letzten Post, d.h. das logische Modul gibt den Device-Namen mit. Wenn man allerdings die Verwaltung von (innerhalb einer Session) eindeutigen IDs für Anfragen dem logischen Modul überlässt, muss man diese ggf. mehrfach implementieren, wenn man ein weiteres logisches Modul haben möchte, das auf das gleiche physikalische Gerät zugreift. Außerdem muss das physikalische Modul auch für seine eigenen Anfragen (Login, Session-Status, ggf. "list" als Information, welche Funktionen unterstützt werden) eine eindeutige ID erzeugen und verwalten. Das sowohl im physikalischen als auch im logischen Modul zu implementieren erscheint mir etwas nach doppeltem Aufwand.

Letzteres erscheint mir sinnvoll, weil "call" und "subscribe" zwei recht unterschiedlich strukturierte Anfrage-Antwort Modelle sind, die beide über die gleiche Schnittstelle laufen. Bei call kommt auf eine Anfrage eine Antwort mit der gleichen ID (Polling). Bei "subscribe" dagegen kommen Events ohne ID und man braucht wiederum eine andere Methode, um herauszufinden, wer die Anfrage gestellt hat...

Meine Idee wäre: Das logische Modul übergibt den Namen des aufrufenden Device mittels IOWrite, damit das physikalische Modul diesen in der ID unterbringen kann. Das physikalische Modul baut diesen in die ID ein und stellt sicher, dass IDs eindeutig sind. Write gibt die eindeutige ID an das logische Modul zurück.

Zitat
Das Problem mit dem Antwort zurueckschicken habe ich leider nicht wirklich verstanden.
Vielleicht hilft etwas Pseudo-Code:

sub Phys_Write
{
    my $id = createID();
    if(USE_ASYNC_METHOD)
    {
        sendRequest($id);
    }
    else
    {
        $ret = execProgram();
        Dispatch($id . ":" . $ret);
    }
    return $id;
}

sub Phys_Read
{
    Dispatch($ret_containing_id);
}

sub Log_Parse
{
    # Find device which asked for $id
}

sub Log_sendRequest
{
    $id = IOWrite(...)
    $some_table{id} = $hash->{NAME} # Remember who asked.
}

Wenn das physikalische Gerät Anfragen asynchron behandelt (z.B. über HTTP), gibt obiger Code kein Problem:

Anfrage stellen: Log_sendRequest - > IOWrite -> Phys_Write -> sendRequest; die $id wird an Log_sendRequest zurückgegeben und gespeichert. Wenn die Antwort kommt: Phys_Read -> Dispatch -> Log_Parse und letzteres vergleicht mit der gespeicherten ID.

Wenn die Anfrage synchron erfolgt (z.B. über ein Kommandozeilen-Programm), passiert aber folgendes:

Log_sendRequest - > IOWrite -> Phys_Write -> execProgram, Dispatch -> Parse; jetzt wird Parse aufgerufen und die ID übergeben, bevor Write die ID als Rückgabewert an das logische Modul zurückgegeben hat und letzteres sie sich merken konnte. Parse kann die ID also nicht finden.

Aber auch ohne dieses ID-Problem ist es wohl keine gute Idee, Dispatch direkt in Write aufzurufen, vermute ich mal. Wenn nun das logische Modul als Folge von Parse eine weitere Anfrage stellen möchte und erneut IOWrite aufruft, kommt es dann nicht zu einer Rekursions-Schleife, wenn sich beide ständig gegenseitig aufrufen? Ich habe den synchronen Fall jetzt so "entschärft", dass Write nicht direkt Dispatch aufruft, sondern einen InternalTimer von 1 Sekunde setzt, der dann Dispatch aufruft.

Könnte man alternativ auch direkt in Write die Antwort zurückgeben? Dann müsste aber das logische Modul unterscheiden können, ob es von IOWrite gerade die komplette Antwort bekommen hat, oder nur eine ID, mit der es später via Parse die Antwort bekommt.

rudolfkoenig

Der IOWrite Aufrifer bekommt doch die Antwort von WriteFn, das hast Du doch selbst festgestellt, siehe oben.
Dispatch in WriteFn aufzurufen ist auch OK, nur ParseFn muss sich benehmen, und in diesem Fall nicht wieder IOWrite aufrufen.

Viel schlimmer ist "$ret = execProgram()", weil das potentiell blockiert.
Richtigerweise ruft man das Programm mit $fd = system("programm|") auf, und man packt $fd ins selectList, damit ReadFn damit aufgerufen wird. Alternativ verwendet man BlockingCall.

xenos1984

Zitat von: rudolfkoenig am 20 September 2021, 14:42:52
Der IOWrite Aufrifer bekommt doch die Antwort von WriteFn, das hast Du doch selbst festgestellt, siehe oben.
Dispatch in WriteFn aufzurufen ist auch OK, nur ParseFn muss sich benehmen, und in diesem Fall nicht wieder IOWrite aufrufen.
Ja, aber WriteFn gibt die ID an den IOWrite Aufrufer zurück, nachdem WriteFn die Antwort des Calls über Dispatch an ParseFn gegeben hat. In dem Moment wartet ParseFn aber noch gar nicht auf diese Antwort, die zu dieser ID gehört, weil es die ID noch nicht bekommen und in seine "meine IDs auf deren Antwort ich warte" Liste eingetragen hat.

Zitat
Viel schlimmer ist "$ret = execProgram()", weil das potentiell blockiert.
Richtigerweise ruft man das Programm mit $fd = system("programm|") auf, und man packt $fd ins selectList, damit ReadFn damit aufgerufen wird. Alternativ verwendet man BlockingCall.
Ah, da hast du natürlich Recht. Die Syntax $fd = system("programm|") ist mir in der Form noch nie begegnet... Hast du ein Beispiel, wo die in FHEM benutzt wird, oder einen Link, wo sie beschrieben wird? In der Perl-Doku konnte ich nur system() als blockierende Funktion finden, die den Exit-Status zurückliefert, aber keinen Dateideskriptor. Um die Programmausgabe zu lesen, verweist die Perl-Doku auf qx, was aber auch blockiert.

rudolfkoenig

ZitatDie Syntax $fd = system("programm|") ist mir in der Form noch nie begegnet...
Sorry, mein Fehler, ich habe es mit open verwechselt.
Siehe auch https://perldoc.perl.org/functions/open#Opening-a-filehandle-into-a-command

xenos1984

Nach einigem Einlesen und Probieren habe ich jetzt einmal versucht, ein zweistufiges Modul zu implementieren, von denen das physikalische Modul UBUS_CLIENT die Kommunikation mit dem uBus-Gerät verwaltet und das logische Modul UBUS_CALL einen Funktionsaufruf ausführt. Damit ist es jetzt möglich, individuell zu konfigurieren, welche Aufrufe ausgeführt werden sollen, in welchem zeitlichen Abstand und wie die Antwort in Readings umgesetzt werden soll (dafür kann Perl-Code angegeben werden).

NB! Das ist zuerst einmal nur ein Proof-of-Concept, um zu testen, ob da überhaupt Daten rauskommen und man die in Readings verarbeiten kann. Es sind noch ganz viele Sachen auf der ToDo-Liste, die ich noch nicht umgesetzt habe:

  • Richtige Implementierung von enable/disable, um die Module zeitweise anzuhalten.
  • Periodische "keepalive" Aufrufe im physikalischen Modul.
  • Automatischer neuer Login wenn die Verbindung verloren gegangen ist.
  • Timeout und Fehlerbehandlung.
  • Nicht-blockierender Aufruf der Kommandozeilen-Version.
  • Implementierung von subscribe / ereignisgesteuerte Kommunikation.
  • Perl-Code in der Definition des logischen Geräts, um dynamische Parameter zu übergeben.
  • Arrays statt einzelner Parameter-Werte, um die gleiche Funktion mehrfach aufzurufen (z.B. um den Status mehrerer Netzwerk-Schnittstellen in einem Gerät abzurufen).

Die Benutzung ist in der Commandref dokumentiert - Beispiele kommen noch.


UBUS_CALL
[EN DE]

    The uBus IPC/RPC system is a common interconnect system used by OpenWrt. Services can connect to the bus and provide methods that can be called by other services or clients or deliver events to subscribers. This module implements the "call" type request. It is supposed to be used together with an UBUS_CLIENT device, which must be defined first.
    Define

    define <name> UBUS_CALL <module> <function> [<parameters>]

    uBus calls are grouped under separate modules or "paths". In order to call a particular function, one needs to specify this path, the function to be called and optional parameters as <key>=<value> pairs. Examples:

        define <name> UBUS_CALL system board

        define <name> UBUS_CALL iwinfo devices

        define <name> UBUS_CALL network.device status name=eth0

        define <name> UBUS_CALL file list path=/tmp

        define <name> UBUS_CALL file read path=/etc/hosts

    The supported calls highly depend on the device on which the uBus daemon is running and its firmware. To get an overview of the calls supported by your device, consult the readings of the UBUS_CLIENT device which represents the connection to the physical device.
    Set

        set <name> disable

        Sets the state of the device to inactive, disables periodic updates and disconnects a websocket connection.

        set <name> enable

        Enables the device, so that automatic updates are performed.

        set <name> update

        Performs an uBus call, updates the corresponding readings and resets any pending interval timer.
    Get

    There are no get commands defined.
    Attributes
        disable
        disabledForIntervals

        attr <name> interval <interval>

        Defines the interval (in seconds) between performing consecutive calls and updating the readings.

        attr <name> readings {<Perl-code>}

        Perl code which must return a hash of <key> => <value> pairs, where <key> is the name of the reading and <value> is its value. The following variables are available in the code:
            $NAME: name of the UBUS_CALL device.
            $MODULE: module name used in the call (see definition).
            $FUNCTION: function name used in the call (see definition).
            %PARAMS: hash of parameters used in the call (see definition).
            $RAW: raw JSON response returned by the call.
            $ERROR: reported error code, 0 means success.
            %DATA: decoded result data as Perl hash.

        If this attribute is omitted, its default value is {FHEM::UBUS_CALL::DefaultReadings($RAW)}. This function executes json2nameValue in the JSON result and turns all returned data into readings named by their position in the JSON tree. It is also possible to call this function in user-defined Perl code first, and then modify the returned hash, for example by deleting unwanted readings or adding additional, computed readings.
    Readings

    Any readings are defined by the attribute readings.

UBUS_CLIENT
[EN DE]

    The uBus IPC/RPC system is a common interconnect system used by OpenWrt. Services can connect to the bus and provide methods that can be called by other services or clients or deliver events to subscribers. This module provides different methods to connect to an uBus interface, either using its command line interface or remotely via websocket or HTTP.
    Define

    define <name> UBUS_CLIENT <method>

    Three possible connection methods for <method> are supported:
        For a websocket connection, a url of the form (ws|wss)://<host>[:port][/path] is used. Example:

        define <name> UBUS_CLIENT ws://192.168.1.1

        For a HTTP connection, a url of the form (http|https)://<host>[:port][/path] is used. Example:

        define <name> UBUS_CLIENT http://192.168.1.1/ubus

        To use the ubus command line tool (if FHEM is running on the same device as ubus), use ubus. Example:

        define <name> UBUS_CLIENT ubus

    When using the websocket or HTTP connection methods, a valid user name and password must be provided. The user name defaults to user, but can be changed with an attribute:

    attr <name> username <username>

    The password is set with the following command, which must be issued only once, and stored as an obfuscated value on disk:

    set <name> password <password>

    When a connection and login have been performed successfully, a list command is executed to obtain the available calls supported by this device, and the result is filled into the readings of the device.
    Set

        set <name> disable

        Sets the state of the device to inactive, disables periodic updates and disconnects a websocket connection.

        set <name> enable

        Enables the device, establishing a websocket connection first if necessary.

        set <name> password <password>

        Sets the password used to authenticate via websocket or HTTP and stores it on disk.
    Get

    There are no get commands defined.
    Attributes
        disable
        disabledForIntervals

        attr <name> username <username>

        Defines the username to be used for login via websocket or HTTP. The default value is user.
    Readings

    When the connection is established, the module executes a list command and creates the following readings:
        mod_<n>_name: name (path) of the n'th module in the uBus tree
        mod_<n>_func_<m>_name: name of the m'th function supported by the n'th module
        mod_<n>_func_<m>_param_<k>_name: name of the k'th parameter of the m'th function of the n'th module
        mod_<n>_func_<m>_param_<k>_type: type of the k'th parameter of the m'th function of the n'th module

    These can be used to perform calls using the UBUS_CALL module.

xenos1984

Ich habe wieder ein wenig an meinen UBUS-Modulen gebastelt und langsam nähern sie sich der Praxistauglichkeit.

Erledigt:

  • Richtige Implementierung von enable/disable, um die Module zeitweise anzuhalten.
  • Periodische "keepalive" Aufrufe im physikalischen Modul.
  • Automatischer neuer Login wenn die Verbindung verloren gegangen ist.
  • Perl-Code in der Definition des logischen Geräts, um dynamische Parameter zu übergeben.
  • Arrays statt einzelner Parameter-Werte, um die gleiche Funktion mehrfach aufzurufen (z.B. um den Status mehrerer Netzwerk-Schnittstellen in einem Gerät abzurufen).
  • attr IODev zur Wahl des physikalischen Gerätes, wenn es mehrere gibt. (Ungetestet, da ich nur eines habe.)
  • Reading state bei UBUS_CALL gibt Aufschluss über den Status des Funktionsaufrufs.

ToDo:

  • Timeout und Fehlerbehandlung.
  • Nicht-blockierender Aufruf der Kommandozeilen-Version. (Da die scheinbar eine andere API benutzt und ich kein Gerät zum Testen habe, ist die erst einmal weiter hinten auf der Liste, und ich werde sie erst einmal deaktivieren.)
  • Implementierung von subscribe / ereignisgesteuerte Kommunikation. (Erfordert ebenfalls noch mehr Input.)

Die Commandref aktualisiere ich noch, aber wer schon einmal einen Blick werfen möchte, oder bei entsprechender Hardware auch testen, ist herzlich dazu eingeladen. Beispiele (können je nach verwendeter Hardware anders aussehen) - die Readings des physikalischen Geräts (UBUS_CLIENT) geben eine Info):

Physikalisches Gerät:


defmod juci UBUS_CLIENT ws://192.168.1.1


Status aller Netzwerk-Interfaces:


defmod network_device_status UBUS_CALL network.device status


...
eth0_up true
...


Vorhandene Status-LEDs (wird nur einmal ausgelesen, Komma-separierte Liste wird als Reading leds erzeugt):


defmod router_config_leds UBUS_CALL uci get config=leds
attr router_config_leds interval 0
attr router_config_leds readings {\
my $r = FHEM::UBUS_CALL::DefaultReadings($RAW);;\
$r->{leds} = join(',', map {$r->{$_}} grep(/values_.*_\.name/, keys %{$r}));;\
$r\
}


...
leds internet,voice,wifi
...


LED Status (Funktion erwartet als Parameter "name" eine LED. Hier wird ein Reading übergeben, das eine Komma-separierte Liste der Namen enthält. Die Funktion wird für jeden Wert einzeln aufgerufen. Bei der Erzeugung der Readings wird dieser Wert als Präfix übergeben.):


defmod router_system_led UBUS_CALL juci.system led_status name={ReadingsVal('router_config_leds','leds','')}
attr router_system_led readings {FHEM::UBUS_CALL::DefaultReadings($RAW, $PARAMS{name} . '_')}


...
wifi_brightness 100
wifi_state off
...


Perl-Code in der Definition kann entweder eine einzelne Zeichenkette zurückgeben oder eine Array-Referenz. Bei letzterem wird für jeden Wert des Arrays ein Funktionsaufruf an das physikalische Modul geschickt. Bei ersterem wird eine Komma-separierte Liste aufgeteilt und ebenfalls für jeden Wert ein Funktionsaufruf generiert, bei einem einzelnen Wert also nur einer. Es dürfen keine Leerzeichen enthalten sein, sonst kommt parseParams durcheinander - längerer Perl-Code sollte also besser in myUtils o.ä. ausgelagert werden.

Es kann passieren, dass UBUS_CALL nach einem Neustart disconnected anzeigt. Das passiert, wenn ein Funktionsaufruf abgesetzt wird, bevor sich der UBUS_CLIENT eingeloggt hat. Beim nächsten Update sollte es dann aber laufen.

xenos1984

Update zum UBUS_CLIENT: Mit


attr <device> refresh 0


kann man nun das periodische "session list" zur Überprüfung, ob die aktuelle Session noch gültig ist, abschalten. Wenn "session list" keine Session zurückliefert, wird auf diese Möglichkeit im Log hingewiesen.

xenos1984

Nachdem es nach ausführlichem Testen über eine Woche stabil im Betrieb war (und ich die Guidelines noch einmal gründlich durchgegangen bin, da es mein erstes Modul ist), habe ich es jetzt im SVN eingecheckt.