Benutzer:StefanStrobel/DevelopmentModuleIntro
Einleitung
Um neue Geräte in FHEM verfügbar zu machen, kann man ein eigenes Modul in Perl schreiben, das automatisch von FHEM geladen wird, wenn ein passendes Gerät in FHEM definiert wird. Das Modul definiert dann wie mit dem Gerät kommuniziert wird, stellt Werte ("Readings") innerhalb von FHEM zur Verfügung oder erlaubt es das Gerät mit "Set"-Befehlen zu beeinflussen. Dieser Text soll den Einstieg in die Entwicklung eigener Module erleichtern.
Mit dem FHEM-Befehl "define", der typischerweise in die zentrale Konfigurationsdatei fhem.cfg eingetragen wird, werden Geräte in FHEM definiert. Der Befehl sorgt dafür dass ein neues Modul bei Bedarf geladen wird und die Initialisierungsfunktion des Moduls aufgerufen wird.
Damit das funktioniert müssen der Name des Geräts, der Name des Moduls und der Name der Initialisierungsfunktion zueinander passen. Das folgende Beispiel soll dies verdeutlichen:
Ein Jeelink USB-Stick in einer Fritz-Box wird mit dem Befehl define JeeLink1 JeeLink /dev/ttyUSB0@57600
definiert.
In der fhem.pl wird der define-Befehl verarbeitet, geprüft, ob ein Modul mit Namen JeeLink schon geladen ist und falls nicht ein Modul mit Namen XY_JeeLink.pm im Modulverzeichnis (bei einer FritzBox z.B. /var/media/ftp/fhem/FHEM) gesucht und dann geladen. Danach wird die Funktion JeeLink_Initialize aufgerufen. Die Moduldatei muss also nach dem Namen des Geräts benannt werden und eine Funktion mit dem Namen des Geräts und einer _initialize Funktion enthalten. In der Initialisierungsfunktion des Moduls werden dann die Namen der aller weiteren Funktionen des Moduls, die von fhem.pl aus aufgerufen werden, bekannt gemacht. Dazu wird der Hash - das ist die zentrale Datenstruktur für jede Instanz eines Gerätes - mit entsprechenden Werten gefüllt.
Der Hash einer Geräteinstanz
Der zentrale Speicherort für Informationen einer Geräteinstanz ist ein Hash, der seinerseits in fhem.pl von einem globalen Hash referenziert wird.
$defs{$d}
in fhem.pl verweist auf den Hash der Geräteinstanz mit Namen $d
. Diesen Hash bekommen die Funktionen eines Moduls oft von fhem.pl übergeben. In ihm stehen beispielsweise die internen Werte des Geräts, die im GUI als "Internals" angezeigt werden oder die Readings des Geräts.
$hash{NAME}
enthält beispielsweise den Namen der Geräteinstanz, $hash{TYPE}
enthält den Namen des Typs des Geräts, der ja auch für den Namen des Moduls verwendet wird.
Ein Abfrageintervall, das beim define eines Geräts an die define-Funktion im Modul übergeben wird, würde typischerweise als "Internal" in $hash->{INTERVAL}
abgelegt.
Die Readings sind hier als weitere Unterstruktur gespeichert, beispielsweise $hash{READINGS}{$ReadingName}{VAL}
und $hash{READINGS}{$ReadingName}{TIME}
. Innerhalb von fhem.pl könnten die selben Readings beispielsweise über $defs{$d}{READINGS}{$ReadingName}{VAL}
adressiert werden, da $defs{$d}
ja auf den gleichen Hash verweist, der dann den Modul-Funktionen übergeben wird und innerhalb der Funktionen typischerweise als $hash
verwendet wird.
Readings
Werte, die von einem Gerät gelesen werden und in FHEM zur Verfügung stehen werden Readings genannt. Sie werden wie oben beschrieben als Unterstruktur des Hashes der jeweiligen Geräteinstanz gespeichert, beispielsweise $hash{READINGS}{$Temp}{VAL}
für die Temperatur eines Fühlers, die als Reading gespeichert wurde.
Zum Zugriff auf Readings steht auch die Funktion ReadingsVal($$$) zur Verfügung.
Readings werden im statefile von FHEM automatisch zwischengespeichert, damit sie nach einem Neustart sofort wieder zur Verfügung stehen, auch bevor sie vom Modul neu gesetzt oder aktualisiert werden.
Internals
Werte, die das Modul intern als Teil des Hashes speichert, die aber keine Readings sind, nennt man Internals. Sie werden abenfalls als Unterstruktur des Hashes der jeweiligen Geräteinstanz gespeichert, beispielswiese $hash->{INTERVAL}
für ein Abfrageintervall, das beim Define-Befehl übergeben wurde und als Internal gespeichert wird. Internals werden jedoch im Gegensatz zu Readings nicht im statefile zwischengespeichert.
Attribute
Parameter eines Moduls können als so genannte Attribute gesetzt und damit dem Modul zur Verfügung gestellt werden. Attribute werden zusammen mit der Definition der Geräte beim Speichern der aktuellen Konfiguration von FHEM in die Konfigurationsdate geschrieben. Zur Laufzeit werden sie in der globalen Datenstruktur $attr{$name}
gespeichert. Ein Attribut mit dem Namen header
würde beispielswiese mit $attr{$name}{header}
adressiert ($attr{$name}->{'header'}
wäre eine alternative aber unübliche Schreibweise für die selber Variable).
Zum Auslesen solcher Attribute sollte die Funktion AttrVal($$$)
verwendet werden.
Die wichtigsten Funktionen in einem Modul
Eine typische Grundfunktion eines einfachen Moduls ist das Auslesen von Werten von einem physischen Gerät und Bereitstellen dieser Werte innerhalb von FHEM als Readings. Das Geräte könnte beispielsweise an einem USB-Port angeschlossen sein. Folgende Funktionen könnte man beispielsweise in einem Modul mit Namen X implementieren:
- X_Initialize
- X_Define
- X_Undef
- X_Set
- X_Get
- X_Attr
Diese Liste ist jedoch noch nicht vollständig. Weitere Funktionen sollten noch aufgelistet und beschrieben werden.
Die Funktionen werden im folgenden beschrieben:
X_Initialize
sub X_Initialize($) { my ($hash) = @_; ...
Das X
im Namen muss dabei auf den Namen des Moduls bzw. des definierten Gerätetyps geändert werden. Im Modul mit der Datei 36_JeeLink.pm
beispielsweise ist der Name der Funktion JeeLink_Initialize
. Die Funktion wird von Fhem.pl nach dem Laden des Moduls aufgerufen und bekommt einen Hash für das Modul als zentrale Datenstruktur übergeben.
Die Initialize-Funktion setzt dann weitere Funktionsnamen, die im Modul implementiert sind, in diesen Hash:
$hash->{DefFn} = "X_Define"; $hash->{UndefFn} = "X_Undef"; $hash->{SetFn} = "X_Set"; $hash->{GetFn} = "X_Get"; $hash->{AttrFn} = "X_Attr";
X
ist wieder durch den Modulnamen ohne die vorangestellte Zahl zu ersetzen. Darüber hinaus sollten die vom Modul unterstützen Attribute definiert werden:
$hash->{AttrList} = "do_not_notify:1,0 " . "header " . $readingFnAttributes;
In Fhem.pl werden dann die entsprechenden Werte beim Aufruf eines attr
-Befehls in die globale Datenstruktur $attr{$name}
, z.B. $attr{$name}{header}
für das Attribut header
gespeichert. Falls im Modul weitere Aktionen oder Prüfungen beim Setzen eines Attributs nötig sind, dann kann wie im Beispiel oben die Funktion X_Attr
implementiert und in der Initialize-Funktion bekannt gemacht werden.
X_Define
Die Define-Funktion eines Moduls wird von Fhem aufgerufen wenn der Define-Befehl für ein Geräte ausgeführt wird und das Modul bereits geladen und mit der Initialize-Fubktion initialisiert ist. Sie ist typischerweise dazu da, die übergebenen Parameter zu prüfen und an geeigneter Stelle zu speichern sowie einen Kommunikationsweg zum Gerät zu öffnen (z.B. TCP-Verbindung, USB-Schnittstelle o.ä.) Sie beginnt typischerweise mit:
sub HTTPMOD_Define($$) { my ( $hash, $def ) = @_; my @a = split( "[ \t][ \t]*", $def ); ...
Als Übergabeparameter bekommt die Define-Funktion den Hash der Geräteinstanz sowie den Rest der Parameter, die im Befehl angegeben wurden. Welche und wie viele Parameter akzeptiert werden ist Sache dieser Funktion. Im obigen Beispiel wird alles nach dem übergebenen Hash in ein Array aufgeteilt und so können die vom Modul bzw. der Define-Funktion erwarteten Werte über das Array der Reihe nach verarbeitet werden:
my $name = $a[0]; my $url = $a[2]; my $inter = 300; if(int(@a) == 4) { $inter = $a[3]; if ($inter < 5) { return "interval too small, please use something > 5, default is 300"; } }
Damit die übergebenen Werte auch anderen Funktionen zur Verfügung stehen und an die jeweilige Geräteinstanz gebunden sind, werden die Werte typischerwiese als Internals im Hash der Geräteinstanz gespeichert:
$hash->{url} = $url; $hash->{Interval} = $inter;
Wenn eine physische Schnittstelle geöffnet werden soll und dann bei verfügbaren Eingabedaten eine Lese-Funktion von Fhem aufgerufen werden soll, dann kann man in der Define-Funktion die Funktion DevIo_OpenDev aufrufen, die sich um alles weitere kümmert. Sie öffnet die Schnittstelle und fügt den Filedeskriptor an die globale Liste offener Verbindungen (selectlist / readyfnlist) an. Damit kann Fhem in seiner Hauptschleife erkennen, von welchem Gerät Daten bereit stehen und die zuständigen Funktionen aufrufen:
my $ret = DevIo_OpenDev( $hash, 0, "X_DevInit" );
Die optionale Funktion X_DevIni
t wird zur weiteren Initialisierung der Verbindung von DevIo_OpenDev
aufgerufen. Der zweite Übergabeparameter an DevIo_OpenDev
(hier 0
) steht für reopen und wird benötigt, da die Funktion auch aufgerufen wird, wenn ein USB-Geräte beispielsweise im Betrieb aus- und wieder eingesteckt wird. In diesem Fall wird die Funktion mit 1
aufgerufen.
X_Undef
Die Undef
-Funktion ist das Gegenstück zur Define
-Funktion und wird aufgerufen wenn ein Gerät mit delete
gelöscht wird. Entsprechend müssen in der Funktion typische Aufräumarbeiten durchgeführt werden wie das saubere Schließen von Verbindungen oder das entfernen von internen Timern sofern diese im Modul zum Pollen verwendet wurden (siehe später).
Beispiel:
sub WKRCD4_Undef($$) { my ( $hash, $arg ) = @_; DevIo_CloseDev($hash); RemoveInternalTimer($hash); return undef; }
X_Get
Die Get-Funktion wird aufgerufen wenn der Fhem-Befehl get
mit einem Gerät dieses Moduls ausgeführt wird. Mit get
werden typischerweise Werte von einem Gerät abgefragt. Einige Module verwenden für diese Funktion einen Hash im Modul, der die möglichen get
-Optionen mit zusätzlichen Werten definiert:
my X_gets = ( "TempSoll" => "XY", "Steilheit" => "Z" );
In der Get-Funktion selbst werden dann die übergebenen Parameter gegen diesen Hash geprüft.
Beispiel:
sub X_Get($@) { my ( $hash, @a ) = @_; return "\"get X\" needs at least one argument" if ( @a < 2 ); my $name = shift @a; my $opt = shift @a; if(!$X_gets{$opt}) { my @cList = keys %X_gets; return "Unknown argument $attr, choose one of " . join(" ", @cList); } ...
Die Ausgabe der Meldung mit ... choose one of ...
ist dabei wichtig, da sie im GUI-Modul verwendet wird um die möglichen get
-Optionen zu ermitteln und als Auswahl anzubieten. Im weiteren Verlauf der Get-Funktion könnte man dann mit dem physischen Gerät kommunizieren und den gefragten Wert abfragen und diesen als Return-Wert der Get-Funktion zurückgeben.
X_Set
Die Set-Funktion ist das Gegenteil zur Get-Funktion. Sie ist dafür gedacht, Werte zum physischen Gerät zu schicken. Falls nur interne Werte im Modul gesetzt werden sollen, so sollte statt Set die Attr-Funktion verwendet werden. Attribute werden bei Save-Config auch in der Fhem.cfg gesichert. Set-Befehle nicht.
Eine Set-Funktion ist ähnlich aufgebaut wie die Get-Funktion, sie bekommt jedoch nach dem Namen der Option auch den zu setzenden Wert übergeben.
Beispiel:
sub X_Set($@) { my ( $hash, @a ) = @_; return "\"set X\" needs at least an argument" if ( @a < 2 ); my $name = shift @a; my $opt = shift @a; my $arg = join("", @a); if(!defined($X_sets{$attr})) { my @cList = keys %X_sets; return "Unknown argument $attr, choose one of " . join(" ", @cList); }
X_Attr
Die Attr-Funktion implementiert Prüfungen der bei einem attr
übergebenen Werte und eventuell zusätzliche Aktionen wenn ein Attribut gesetzt wird. Die Liste der möglichen Attribute wird in der X_Initialize-Funktion
definiert (siehe oben). Fhem ruft bei einem Attr-Befehl die zuständige X-Attr-Funktion
auf und wenn diese keine Fehlermelhung sondern undef
zurückgibt, dann schreibt fhem.pl die bei attr
angegebenen Werte in die jeweilige Datenstruktur $attr{$name}-> ...
Beispiel:
HTTPMOD_Attr(@) { my ($cmd,$name,$aName,$aVal) = @_; # $cmd can be "del" or "set" # $name is device name # aName and aVal are Attribute name and value if ($cmd eq "set") { if ($aName eq "Regex") { eval { qr/$aVal/ }; if ($@) { Log3 $name, 3, "X: Invalid regex in attr $name $aName $aVal: $@"; return "Invalid Regex $aVal"; } } } return undef; }
Die Attr-Funktion bekommt nicht den Hash der Geräteinstanz übergeben, da sie ja auch keine Werte dort speichern muss sondern den Befehl set
oder del
je nachdem ob ein Attribut gesetzt oder gelöscht wird, den Namen der Geräteinstanz sowie den Namen des Attributs und seinen Wert.
Im obigen Beispiel wird für ein Attribut mit Namen Regex geprüft ob die Regex fehlerhaft ist. Falls sie ok ist, wird undef zurück gegegen und fhem.pl speichert den Wert des Attributs.
Pollen von Geräten
Wenn Geräte von sich aus keine Informationen senden sondern abgefragt werden müssen, kann man im Modul die Funktion InternalTimer
verwenden. Man übergibt ihr den Zeitpunkt für den nächsten Aufruf, den Namen der Funktion, die aufgerufen werden soll, die zu übergebenden Parameter und ein Flag ob der erste Aufruf verzögert werden soll falls die Initialiserung des Geräts noch nicht abgeschlossen ist.
Beispielsweise könnte man für das Abfragen eines Geräts in der Define-Funktion den Timer folgendermassen setzen:
# initial request after 2 secs, there timer is set to interval for further update
InternalTimer(gettimeofday()+2, "X_GetUpdate", $hash, 0);
in der Funktion X_GetUpdate
selbst wird dann der Timer neu gesetzt, so dass nach einem Intervall die Funktion erneut aufgerufen wird:
sub X_GetUpdate($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
InternalTimer(gettimeofday()+$hash->{Interval}, "X_GetUpdate", $hash, 1);
Log3 $name, 4, "X: GetUpdate called ...";
Im weiteren Verlauf der Funktion könnte man dann das Gerät abfragen und die abgefragten Werte in Readings speichern
Lesen von Geräte und Speichern in Readings
muss noch beschrieben werden ...
Logging / Debugging
Um Innerhalb eines Moduls eine Protokollmeldung in die Fhem-Logdatei zu schreiben, wird die Funktion Log3 aufgerufen:
Log3 $name, 3, "X: Problem erkannt ...";
Die Parameter der Funktion Log3 sind der Name der Geräteinstanz, das Verbose-Level, in dem die Meldung sichtbar sein soll und die Meldung selbst.
Den Namen der Geräteinstanz kann man in den Funktionen, die den Hash übergeben bekommen einfach aus diesem Hash nehmen:
my $name = $hash->{NAME};
Um für ein neues Modul das Verbose-Level zu erhöhen, ohne gleich für das Gesamte FHEM alle Meldungen zu erzeugen kann man den Befehl
attr gerätename verbose
verwenden. Beispielsweise attr PM verbose 5
Damit bietet es sich an im Modul Meldungen, die im normalen Betrieb nicht benötigt werden, beim Aufruf von Log3 mit dem Level 4 oder 5 anzugeben. Wenn man dann bei der Fehlersuche mehr Meldungen sehen möchte, erhöht man mit attr X verbose das Level für das betroffene Gerät.