DevelopmentModuleIntro: Unterschied zwischen den Versionen

Aus FHEMWiki
Zeile 1.195: Zeile 1.195:
</source>
</source>


Siehe dazu Forumsbeitrag: {{Link2Forum|Topic=33422|Message=258272}}
Siehe dazu Forumsbeitrag: {{Link2Forum|Topic=33422}}
}}
}}
Die Match-Liste ordnet eine Nachrichtensyntax (regulärer Ausdruck) einem Modulnamen zu. Sollte eine Nachricht vom physikalischen Gerät empfangen werden, die durch kein geladenes Modul verarbeitet werden kann, so wird über die Match-Liste geprüft, welches Modul diese Nachricht verarbeiten kann. Dieses Modul wird anschließend geladen und die Nachricht durch dieses verarbeitet. In dieser Liste findet mittels regulärem Ausdruck eine Zuordnung der Nachrichtenstruktur zum verarbeitenden logischen Modul statt.
Die Match-Liste ordnet eine Nachrichtensyntax (regulärer Ausdruck) einem Modulnamen zu. Sollte eine Nachricht vom physikalischen Gerät empfangen werden, die durch kein geladenes Modul verarbeitet werden kann, so wird über die Match-Liste geprüft, welches Modul diese Nachricht verarbeiten kann. Dieses Modul wird anschließend geladen und die Nachricht durch dieses verarbeitet. In dieser Liste findet mittels regulärem Ausdruck eine Zuordnung der Nachrichtenstruktur zum verarbeitenden logischen Modul statt.

Version vom 30. Oktober 2016, 21:36 Uhr

Info blue.png
Dieser Text ist in Arbeit und muss noch an einigen Stellen ergänzt werden. Insbesondere beschreibt der Text derzeit nur einstufige Module. Die Abgrenzung zu zweistufigen Modulen und deren Eigenschaften sollte noch ergänzt werden.



Einleitung

Um neue Geräte, Dienste, o.ä. in FHEM verfügbar zu machen, kann man ein eigenes Modul in Perl schreiben. Ein Modul wird in FHEM automatisch geladen, wenn ein entsprechendes Device in FHEM definiert wird. Das Modul ermöglicht eine spezifische Kommunikation mit einem physikalischen Gerät, stellt Ergebnisse ("Readings") und Events innerhalb von FHEM zur Verfügung und erlaubt es, das Gerät mit "Set"-/"Get"-Befehlen zu beeinflussen. Dieser Artikel soll den Einstieg in die Entwicklung eigener Module erleichtern.

Mit dem FHEM-Befehl define werden Devices in FHEM basierend auf einem Modul definiert. Dieser Befehl sorgt dafür, dass ein neues Modul bei Bedarf geladen und initialisiert wird. Ein gutes Beispiel ist hierbei die zentrale Konfigurationsdatei "fhem.cfg" in der sämtliche Devices in Form von define-Statements gespeichert sind.

Damit das funktioniert müssen der Name des Moduls und der Name der Initialisierungsfunktion identisch sein. Das folgende Beispiel soll dies verdeutlichen:

Ein Jeelink USB-Stick könnte beispielsweise mit dem Befehl define JeeLink1 JeeLink /dev/ttyUSB0@57600 definiert werden.

In fhem.pl wird der define-Befehl verarbeitet und geprüft, ob ein Modul mit dem Namen "JeeLink" schon geladen ist und falls nicht ein Modul mit Namen XY_JeeLink.pm im Modulverzeichnis (z.B. /opt/fhem/FHEM) gesucht und dann geladen. Danach wird die Funktion JeeLink_Initialize() aufgerufen um das Modul in FHEM zu registrieren. Eine Moduldatei muss dazu eine Funktion <Modulname>_Initialize() enthalten. Durch den Aufruf dieser Funktion wird FHEM mitgeteilt, welche Funktionalitäten dieses Modul unterstützt.

In der Initialisierungsfunktion des Moduls werden die Namen aller weiteren Perl-Funktionen des Moduls, die von fhem.pl aus aufgerufen werden, bekannt gemacht. Dazu wird für jedes Modul ein eigener Hash (genauer "Modul-Hash") mit entsprechenden Werten gefüllt, der in fhem.pl für jedes Modul entsprechend abgelegt wird. Dadurch weiß FHEM wie dieses Modul anzusprechen ist.

Der Hash einer Geräteinstanz

Eine Besonderheit in Perl sind assoziative Arrays, (nicht ganz richtig als "Hash" bezeichnet) in denen die Adressierung nicht über eine Zählvariable erfolgt, sondern über einen beliebigen String. Die internen Abläufe bei der Adressierung führen dazu, dass die Speicherung in und der Abruf aus Hashes relativ langsam ist.

Der zentrale Speicherort für Informationen einer Geräteinstanz bei FHEM ist ein solcher Hash, der seinerseits in fhem.pl von einem globalen Hash referenziert wird.

In fhem.pl werden alle Gerätedefinitionen in dem globalen Hash %defs abgelegt. Der Inhalt von $defs{<Name>} in fhem.pl verweist dabei auf den Hash der Geräteinstanz in Form einer Hashreferenz. Diesen Verweis (also nur die Adresse) bekommen die Funktionen eines Moduls übergeben (i.d.R. als $hash bezeichnet), welche direkt von fhem.pl aufgerufen werden. In dem Hash stehen beispielsweise die internen Werte des Geräts, die im Frontend als "Internals" angezeigt werden, sowie die Readings des Geräts.

Beispiele:

  • $hash->{NAME} enthält den Namen der Geräteinstanz,
  • $hash->{TYPE} enthält die Typbezeichnung des Geräts (Modulname)

Ausführung von Modulen

FHEM arbeitet intern nicht parallel, sondern arbeitet alle Aufgaben seriell nacheinander kontinuierlich ab. Daher wäre es ungünstig, wenn Module Daten von einem physikalischen Gerät abfragen wollen und dabei innerhalb der selben Funktion auf die Antwort des Geräts warten. In dieser Zeit, in der FHEM auf die Antwort des Gerätes warten muss, wäre der Rest von FHEM blockiert. Da immer nur eine Aufgabe zur selben Zeit bearbeitet wird, müssen alle weiteren Aufgaben solange warten. Eine Datenkommunikation innerhalb eines Moduls sollte daher immer ohne Blockierung erfolgen. Dadurch kann FHEM die Wartezeit effizient für andere Aufgaben nutzen um bspw. anstehende Daten für andere Module zu verarbeiten. Es gibt in FHEM entsprechende Mechanismen, welche eine "Non-Blocking"-Kommunikation über verschiedene Wege (z.B. seriell, HTTP, TCP, ...) ermöglichen.

Dafür werden in FHEM zwei zentrale Listen gepflegt, in der die Filedeskriptoren der geöffneten Kommunikatonsverbindungen gespeichert sind. Auf Linux- bzw. Unix-basierten Plattformen wird der select-Befehl des Betriebssystems verwendet um Filedeskriptoren auf lesbare Daten zu überprüfen. In FHEM gibt es dazu eine Liste (%selectlist), in der die Filedeskriptoren sämtlicher Geräte (z.B. serielle Verbindung, TCP-Verbindung, etc.) gespeichert sind.

In der zentralen Schleife (Main-Loop) von fhem.pl wird mit select() überwacht, ob über eine der geöffneten Schnittstellen Daten zum Lesen anstehen. Wenn dies der Fall ist, dann wird die Lesefunktion (X_Read) des zuständigen Moduls aufgerufen, damit es die Daten entgegennimmt und verarbeitet. Anschließend wird die Schleife weiter ausgeführt.

Auf Windows-Systemen funktioniert dies anders. Hier können USB/Seriell-Geräte nicht per select() überwacht werden. In FHEM unter Windows werden daher diese Schnittstellen kontinuierlich abgefragt ob Daten bereitstehen. Dafür müssen Module zusätzlich zur Lesefunktion eine Abfragefunktion (X_Ready) implementieren, welche prüft, ob Daten zum Lesen anstehen. Auch auf Linux/Unix-Plattformen hat diese Funktion eine Aufgabe. Falls nämlich eine Schnittstelle ausfällt, beziehungsweise ein CUL oder USB-zu-Seriell Adapter ausgesteckt wird, dann wird über diese Funktion regelmäßig geprüft ob die Schnittstelle wieder verfügbar wird.

Innerhalb der eigentlichen Lesefunktion (X_Read) werden dann die Daten vom zugehörigen Gerät gelesen, das nötige Protokoll implementiert um die Daten zu interpretieren und Werte in Readings geschrieben.

Auch wenn von einem Anwender über einen Get-Befehl Daten aktiv von einem Gerät angefordert werden, sollte nicht blockierend gewartet werden. Eine asynchrone Ausgabe, sobald das Ergebnis vorliegt, ist über asyncOutput() möglich. Siehe Beschreibung und Beispiel. Weitere Anwendungsbeispiele finden sich im PLEX Modul und im überarbeiteten und nicht-blockierenden SYSSTAT Modul.

Wichtige globale Variablen aus fhem.pl

Internals

Daten, die ein Modul im Geräte-Hash speichert nennt man Internals. Sie werden als Unterstruktur des Hashes der jeweiligen Geräteinstanz gespeichert, beispielswiese $hash->{NAME} für den Gerätenamen, welcher beim Define-Befehl übergeben wurde und als Internal gespeichert wird. Diese Daten spielen für FHEM eine sehr wichtige Rolle, da sämtliche gerätespezifischen Daten als Internal im Gerätehash gespeichert werden.

Falls Werte wie z.B. ein Intervall nicht über den Define-Befehl gesetzt werden sollen und im Betrieb einfach änderbar sein sollten, ist eine alternative Möglichkeit die Speicherung in so genannten Attributen. Dann würde man den Define-Befehl so implementieren, dass er kein Intervall übergeben bekommt und statt dessen das Interval als Attribut über den Befehl attr gesetzt wird.

Generell werden alle Werte, welche direkt in der ersten Ebene von $hash (Gerätehash) gespeichert werden auf der Detail-Seite einer Definition in der FHEMWEB Oberfläche angezeigt. Es gibt jedoch Ausnahmen:

  • $hash->{helper}{URL] - Alle Elemente, welche als Unterelement wieder einen Hash besitzen werden nicht in FHEMWEB dargestellt. Typischerweise speichern Module Daten unter $hash->{helper} interne Daten zwischen, die für den User nicht relevant sind, sondern nur der internen Verarbeitung dienen.
  • $hash->{.ELEMENT} - Alle Knoten, welche mit einem Punkt beginnen werden in der FHEMWEB Oberfläche nicht angezeigt. Man kann diese Daten jedoch beim Aufruf des list-Kommandos einsehen.

Es gibt bereits vorbelegte Internals welche in FHEM dazu dienen definitionsbezogene Informationen wie bspw. Namen und Readings zu speichern. Dies sind im besonderen:

Internal Beschreibung
$hash->{NAME} Der Definitionsname, mit dem das Gerät angelegt wurde.
$hash->{READINGS} Enthält alle aktuell vorhandenen Readings. Daten unterhalb dieses Knotens sollte man nicht direkt manipulieren. Um Readings zu Erzeugen gibt es entsprechende Reading-Funktionen.
$hash->{NR} Die Positions-Nr. der Definition innerhalb der Konfiguration. Diese dient dazu die Konfiguration in der gleichen Reihenfolge zu speichern, wie die einzelnen Geräte angelegt wurden.
$hash->{TYPE} Der Modulname, mit welchem die Definition angelegt wurde.
$hash->{DEF} Sämtliche Argumente, welche beim define-Befehl nach dem Modulnamen übergeben wurden.
$hash->{CFGFN} Der Dateiname der Konfigurationsdatei in der diese Definition enthalten ist (sofern nicht in fhem.cfg). Dieser Wert ist nur gefüllt, wenn man mit mehreren Konfigurationsdateien arbeitet, welche dann in fhem.cfg via include-Befehl eingebunden werden.
$hash->{NTFY_ORDER} Sofern das Modul Events via NotifyFn verarbeitet enthält jede Definition eine Notify-Order als Zeichenkette bestehend aus dem Notify Order Prefix und dem Definitionsnamen. Details zur Funktionsweise gibt es in der Beschreibung zur Notify-Funktion im Abschnitt "Reihenfolge für den Aufruf der Notify-Funktion beeinflussen".
$hash->{NOTIFYDEV} Sofern das Modul Events via NotifyFn verarbeitet kann man damit die Definitionen, von denen man Events erhalten will begrenzen. Details zur Funktionsweise gibt es in der Beschreibung zur Notify-Funktion im Abschnitt "Begrenzung der Aufrufe auf bestimmte Geräte".
$hash->{IODev} Hier wird die zugeordnete IO-Definition durch AssignIoPort() gespeichert, welche für den Datentransport und -empfang dieser Definition zuständig ist. Dieser Wert existiert nur bei Modulen die nach dem zweistufigen Modulkonzept arbeiten.
$hash->{CHANGED} Hier werden alle Events kurzzeitig gesammelt, welche für die Eventverarbeitung anstehen. Insbesondere die Reading-Funktionen speichern hier alle Events zwischen um sie nach Abschluss via DoTrigger() zu verarbeiten.
$hash->{FD} Wenn die Definition eine Netzwerkverbindung oder serielle Schnittstelle geöffnet hat (via DevIo), so wird der entsprechende File-Deskriptor in diesem Internal gespeichert. Damit kann FHEM alle geöffneten Filedeskriptoren der entsprechenden Definition zuordnen um bei ankommenden Daten die Definition via Read-Funktion damit zu versorgen.

Generell sollte man die meisten der hier genannten systemweiten Internals nicht modifizieren, da ansonsten die korrekte Funktionsweise von FHEM nicht mehr garantiert werden kann.

Readings

Daten, welche von einem Gerät gelesen werden und in FHEM in einer für Menschen verständlichen Form zur Verfügung gestellt werden können, werden Readings genannt. Sie geben den Status des Gerätes wieder und erzeugen Events innerhalb von FHEM auf die andere Geräte reagieren können. Sie werden als Unterstruktur des Hashes der jeweiligen Geräteinstanz gespeichert, beispielsweise

  • $hash->{READINGS}{temperature}{VAL} für die Temperatur eines Fühlers
  • $hash->{READINGS}{temperature}{TIME} für den Zeitstempel der Messung

Für den lesenden Zugriff auf Readings steht die Funktion ReadingsVal() zur Verfügung. Ein direkter Zugriff auf die Datenstruktur sollte nicht vorgenommen werden.

Readings werden im Statefile von FHEM automatisch auf der Festplatte zwischengespeichert, damit sie nach einem Neustart sofort wieder zur Verfügung stehen. Dadurch ist der letzte Status eines Gerätes vor einem Neustart nachvollziehbar.

Readings, die mit einem Punkt im Namen beginnen, haben eine funktionale Besonderheit. Sie werden im FHEMWEB nicht angezeigt und können somit als "Permanentspeicher" für kleinere Daten innerhalb des Moduls genutzt werden. Um größere Datenmengen permanent zu speichern sollte man jedoch die Funktion setKeyValue() verwenden.

Zum Setzen von Readings sollen

aufgerufen werden. Dabei kann man auch angeben, ob dabei ein Event ausgelöst werden soll oder nicht. Events erzeugen, je nach Hardwareperformance, spürbare Last auf dem System (siehe NotifyFn), das Ändern von Readings ohne dass dabei Events erzeugt werden jedoch nicht.

Eine Sequenz zum Setzen von Readings könnte folgendermaßen aussehen:

readingsBeginUpdate($hash);
readingsBulkUpdate($hash, $readingName1, $wert1 );
readingsBulkUpdate($hash, $readingName2, $wert2 );
readingsEndUpdate($hash, 1);

Attribute

Damit der Nutzer das Verhalten einer einzelnen Gerätedefinition zur Laufzeit individuell anpassen kann, gibt es in FHEM für jede Definition sogenannte Attribute, welche mit dem Befehl attr gesetzt werden können. Diese stehen dann dem Modul unmittelbar zur Verfügung um das Verhalten während der Ausführung zu beeinflussen. Attribute werden zusammen mit dem define-Statemant der Definition beim Speichern der aktuellen Konfiguration von FHEM in die Konfigurationsdatei geschrieben. Beim Neustart werden die entsprechenden Befehle ausgeführt um alle Definition inkl. Attribute wieder anzulegen. Zur Laufzeit werden Attribute in dem globalen Hash %attr mit dem Definitionsnamen als Index ($attr{$name} = $value) gespeichert. Ein Attribut mit dem Namen header würde beispielsweise mit $attr{$name}{header} adressiert. Generell sollte %attr nicht durch direkten Zugriff manipuliert werden.

Zum Auslesen von Attributen sollte die Funktion AttrVal() verwendet werden.

Welche Attribute ein Modul unterstützt muss in der Funktion X_Initialize durch Setzen der Variable $hash->{AttrList} bekannt gemacht werden (siehe unten).#

Wenn beim Setzen von Attributen die Werte geprüft werden sollen oder zusätzliche Funktionalitäten implementiert werden müssen, dann muss dies in der Funktion X_Attr (siehe unten) implementiert werden. Hier kann man bspw. einen Syntaxcheck für Attribut-Werte implementieren um ungültige Werte zurückzuweisen.

Die wichtigsten Funktionen in einem Modul

Damit fhem.pl ein Modul nutzen kann, muss dieses entsprechende Funktionen mit einer vorgegebenen Aufrufsyntax implementieren. Durch die Bekanntgabe dieser modulspezifischen Funktionen können Daten zwischen fhem.pl und einem Modul entsprechend ausgetauscht werden. Es gibt verschiedene Arten von Funktionen die ein Modul anbieten muss bzw. kann, je nach Funktionsumfang.

Folgende Funktion muss ein Modul mit dem beispielhaften Namen "X" mindestens bereitstellen:

  • X_Initialize (initialisiert das Modul und gibt den Namen zusätzlicher Modulfunktionen bekannt, sowie modulspezifische Einstellungen)

Die folgenden Funktionen sind die wichtigsten Funktionen, welche je nach Anwendungsfall zu implementieren sind. Es handelt sich hierbei um die wichtigsten Vertreter, welche in den meisten Modulen Verwendung finden. Nicht alle Funktionen machen jedoch in jedem Modul Sinn. Generell sollte auch hier bei jeder Funktion der Modulname vorangestellt werden um ein einheitliches Namensschema zu gewährleisten. Hier die wichtigsten Modulfunktionen:

  • X_Define (wird beim define aufgerufen)
  • X_Undef (wird beim delete, sowie rereadcfg aufgerufen. Dient zum Abbau von offenen Verbindungen, Timern, etc.)
  • X_Delete (wird beim delete aufgerufen wenn das Gerät endgültig gelöscht wird um weiterführende Aktionen vor dem Löschen durchzuführen)
  • X_Get (wird beim Befehl get aufgerufen um Daten vom Gerät abzufragen)
  • X_Set (wird beim Befehl set aufgerufen um Daten an das Gerät zu senden)
  • X_Attr (wird beim Befehl attr aufgerufen um bspw. Werte zu prüfen)
  • X_Read (wird vom globalen select aufgerufen, falls Daten zur Verfügung stehen)
  • X_Ready (wird unter Windows als ReadFn-Ersatz benötigt bzw. um zu prüfen, ob ein Gerät wieder verfügbar ist)
  • X_Notify (verarbeitet Events innerhalb von FHEM von anderen Geräten)
  • X_DbLog_split (relevant bei DbLog-Nutzung: Split eines Events in Name/Wert/Einheit)
  • X_Rename (wird aufgerufen, wenn ein Gerät umbenannt wird)
  • X_Shutdown (wird beim Herunterfahren von FHEM ausgeführt)

Für das zweistufige Modulkonzept gibt es weiterhin:

  • X_Parse Zustellen von Daten via Dispatch() vom physischen Modul zum logischen Modul zwecks der Verarbeitung.
  • X_Write Zustellen von Daten via IOWrite() vom logischen zum physischen Modul um diese an die Hardware weiterzureichen.

Diese Funktionen werden in diesem Abschnitt genauer beschrieben.

Es gibt noch weitere Funktionen, welche jedoch für spezielle Fälle gedacht sind, auf die im Rahmen dieses Artikels zur Einführung in die Modulprogrammierung jedoch nicht näher drauf eingegangen wird:

  • X_Except
  • X_Copy
  • X_AsyncOutputFn
  • X_State
  • X_Authorize
  • X_Authenticate
  • X_IOWriteFn
  • X_IOOpenFn
  • X_IOCloseFn
  • X_ActivateInform

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 eine leere Hashreferenz für den Initialisierungsvorgang übergeben.

Dieser Hash muss nun von X_Initialize mit allen modulrelevanten Funktionsnamen gefüllt werden. Anschließend wird dieser Hash durch fhem.pl im globalen Hash %modules gespeichert. $modules{ModulName} wäre dabei der Hash für das Modul mit dem Namen ModulName. Es handelt sich also nicht um den oben beschriebenen Hash der Geräteinstanzen sondern einen Hash, der für jedes Modul existiert und modulspezifische Daten wie bspw. die implementierten Modulfunktionen enthält. Die Initialize-Funktion setzt diese Funktionsnamen, in den Hash des Moduls wie folgt:

$hash->{DefFn}         = "X_Define";
$hash->{UndefFn}       = "X_Undef";
$hash->{DeleteFn}      = "X_Delete";
$hash->{SetFn}         = "X_Set";
$hash->{GetFn}         = "X_Get";
$hash->{AttrFn}        = "X_Attr";
$hash->{ParseFn}       = "X_Parse";
$hash->{ReadFn}        = "X_Read";
$hash->{ReadyFn}       = "X_Ready";
$hash->{NotifyFn}      = "X_Notify";
$hash->{DbLog_splitFn} = "X_Notify";
$hash->{RenameFn}      = "X_Rename";
$hash->{ShutdownFn}    = "X_Shutdown";

# für zweistufiges Modulkonzept:
$hash->{ParseFn}      = "X_Parse";
$hash->{WriteFn}      = "X_Write";

Um eine entsprechende Funktion in FHEM bekannt zu machen muss dazu der Funktionsname, wie er im Modul als sub <Funktionsname>() { ... } definiert ist, als Zeichenkette in $hash gesetzt werden. Dabei sollten die entsprechenden Funktionsnamen immer den Modulnamen (in diesem Beispiel X) als Präfix verwenden. Auf diese Weise können sämtliche modulspezifisch implementierten Funktionen wie X_Read, X_Parse etc. durch Zuweisung an $hash->{ReadFn} bzw. $hash->{ParseFn} usw. bekannt gemacht werden.

Darüber hinaus sollten die vom Modul unterstützten Attribute definiert werden:

$hash->{AttrList} =
  "do_not_notify:1,0 " . 
  "header " .
  $readingFnAttributes;

Die Auflistung aller unterstützten modulspezifischen Attribute erfolgt in Form einer durch Leerzeichen getrennten Liste in $hash->{AttrList}}. Es gibt in FHEM globale Attribute, die in allen Gerätedefinitionen verfügbar sind und nur modulspezifische Attribute die jedes Modul via $hash->{AttrList} über die eigene Initialize-Funktion setzt. In fhem.pl werden dann die entsprechenden Attributwerte 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.

Die Variable $readingFnAttributes, die im obigen Beispiel an die Liste der unterstützten Attribute angefügt wird, definiert Attributnamen, die dann zusätzlich gemacht werden, wenn das Modul zum Setzen von Readings die Funktionen readingsBeginUpdate(), readingsBulkUpdate(), readingsEndUpdate() oder readingsSingleUpdate() verwendet. In diesen Funktionen werden Attribute wie event-min-interval oder auch event-on-change-reading ausgewertet. Für Details hierzu siehe commandref zu readingFnAttributes.


Autocreate-Optionen beim zweistufigen Modulkonzept

Des weiteren ist es möglich, das Verhalten von Autocreate über die Initialize-Funktion zu beeinflussen. Y ist durch den Namen der Geräte zu ersetzen. Legt ihr Geräte mit dem Namen LaCrosse an, dann sollte "Y" durch LaCrosse ersetzt werden. Alle Geräte, welche dieses Modul via Autocreate anlegt und deren neue Gerätenamen auf den regulären Ausdruck Y.* matchen, werden mit den hier spezifizierten Optionen angelegt.

  $hash->{AutoCreate} =
        { "Y.*" => { ATTR => "event-min-interval:.*:300 event-on-change-reading:.*", 
                     FILTER => "%NAME", 
                     GPLOT => "temp4hum4:Temp/Hum,"} };
                     autocreateThreshold => "<count>:<timeout>"

Mit ATTR => können vordefinierte Attribute beim Anlegen definiert werden. Der Wert von FILTER wird als Event-Regex verwendet, wenn ein FileLog angelegt wird. Damit kann man steuern, welche Events von dem zugehörigen neuen FileLog geloggt werden sollen. Definiert man das Feld FILTER nicht dann wird kein FileLog automatisch durch Autocreate angelegt. Mit Hilfe von GPLOT kann ein Plot angelegt werden. Mit der Angabe definiert ihr, welche .gplot-Datei verwendet wird. Mittels autocreateThreshold wird beeinflusst, wie oft count(default 2) und in welchem Zeitabstand timeout (default 60 Sekunden) die gleiche Nachricht empfangen werden muss, damit ein Gerät per autocreate angelegt wird. Das Verhalten, kann vom Anwender mittels Attribut autocreateThreshold im device "autocreate" überschrieben werden.


Nutzung von parseParams()

Die Funktion parseParams() unterstützt Modulautoren beim Parsen von Übergabeparametern, welche bei define, get und set Kommandos an die entsprechenden Modulfunktionen übergeben werden. Dadurch lassen sich auf einfache Weise insbesondere komplexe Parameter (wie bspw. Perl-Ausdrücke) parsen.

Diese Zusatzfunktion kann man in der Initialize-Funktion einfach über folgenden Parameter für X_Define, X_Get und X_Set modulweit aktivieren:

$hash->{parseParams} = 1;

Sobald es gesetzt ist wird automatisch durch fhem.pl parseParams() aufgerufen und die an X_Define, X_Get und X_Set übergebenen Parameter ändern sich wie weiter unten in den jeweiligen Funktionen beschrieben.

X_Define

sub X_Define($$)
{
	my ( $hash, $def ) = @_;
	
	...
 
	return $error;
}

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-Funktion 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.ä.) oder einen Status-Timer zu starten. Sie beginnt typischerweise mit:

sub X_Define($$)
{
	my ( $hash, $def ) = @_;
	my @a = split( "[ \t][ \t]*", $def );
	...

Als Übergabeparameter bekommt die Define-Funktion den Hash der Geräteinstanz sowie den die im define-Befehl übergebenen Parameter. Welche bzw. wie viele Parameter akzeptiert werden und welcher Syntax diese entsprechen müssen ist Sache dieser Funktion. Im obigen Beispiel wird die Argumentzeile $def in ein Array aufgeteilt (durch Leerzeichen/Tabulator getrennt) 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 $module = $a[1];
my $url    = $a[2];
my $inter  = 300;

if(int(@a) == 4) { 
	$inter = $a[3]; 
	if ($inter < 5) {
		return "interval too small, please use something > 5s, default is 300 seconds";
	}
}

Damit die übergebenen Werte auch anderen Funktionen zur Verfügung stehen und an die jeweilige Geräteinstanz gebunden sind, werden die Werte typischerweise als Internals im Hash der Geräteinstanz gespeichert:

$hash->{url} 		= $url;
$hash->{Interval}	= $inter;

Sobald alle Parameter korrekt verarbeitet wurden, wird in der Regel die erste Verbindung zum Gerät aufgebaut. Je nach Art des Geräts kann das eine permanente Datenverbindung sein (z.B. serielle Schnittstelle oder TCP-Verbindung) oder das Starten eines regelmäßigen Timers, der zyklisch den Status z.B. via HTTP ausliest.

Sollten im Rahmen der Define-Funktion Syntax-Probleme der Übergabeparameter festgestellt werden oder es kann bspw. keine Verbindung aufgebaut werden, so ist als Funktionsrückgabewert eine entsprechende Fehlermeldung zurückzugeben. Nur wenn alle Übergabeparameter akzeptiert werden, darf undef zurückgegeben werden. Sobald eine Define-Funktion eine Fehlermeldung zurückmeldet, wird der define-Befehl durch FHEM zurückgewiesen und der User erhält die Fehlermeldung, welche die Define-Funktion produziert hat, als Ausgabe zurück.


Verfügbarkeit von Attributen

Während die Define-Funktion ausgeführt wird, sollte man nicht davon ausgehen, dass alle vom Nutzer konfigurierten Attribute via AttrVal() verfügbar sind. Attribute stehen in der Define-Funktion nur dann zur Verfügung, wenn FHEM sich nicht in der Initialisierungsphase befindet (globale Variable $init_done ist wahr; der Nutzer hat die Gerätedefinition modifiziert). Daher sollte man weiterführende Funktion, welche auf gesetzte Attribute angewiesen sind, nur dann in der Define-Funktion starten, wenn $init_done zutrifft.

Andernfalls sollte man den Aufruf in der Notify-Funktion durchführen sobald global:INITIALIZED bzw. global:REREADCFG getriggert wurde:

sub X_Define($$)
{
	my ( $hash, $def ) = @_;
 
	...

	$hash->{NOTIFYDEV} = "global";

	X_FunctionWhoNeedsAttr($hash) if($init_done);
}

sub X_Notify($$)
{
	my ($own_hash, $dev_hash) = @_;
	my $ownName = $own_hash->{NAME}; # own name / hash
 
	return "" if(IsDisabled($ownName)); # Return without any further action if the module is disabled
 
	my $devName = $dev_hash->{NAME}; # Device that created the events
	my $events = deviceEvents($dev_hash, 1);

	if($devName eq "global" && grep(m/^INITIALIZED|REREADCFG$/, @{$events}))
	{
		 X_FunctionWhoNeedsAttr($hash);
	}
}

Dadurch wird die Modulfunktion X_FunctionWhoNeedsAttr() nach dem Start erst aufgerufen, wenn alle Attribute aus der Konfiguration geladen wurden.


Nutzung von parseParams()

Zum Aufteilen und Parsen von $def lässt sich die Funktion parseParams verwenden um die einzelnen Argumente einfach zu parsen. Wenn in X_Initialize $hash->{parseParams} = 1; gesetzt wurde dann wird parseParams automatisch aufgerufen und X_Define ändert sich wie folgt:

sub X_Define($$$)
{
	my ( $hash, $a, $h ) = @_;
	...

Die genauen Möglichkeiten von parseParams sind in dem entsprechenden Artikel dokumentiert.


Nutzung von DevIo

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.

Um DevIo mitzuteilen welche Verbindung genau zu öffnen ist, muss das Internal $hash->{DeviceName} mit einer entsprechenden Syntax gefüllt sein (z.B. "/dev/ttyUSB0@9600", "192.168.2.105:3000", ...). Üblicherweise wird diese Information als Argument im define-Befehl vom Nutzer angegeben.

my $ret = DevIo_OpenDev( $hash, 0, "X_DeviceInit" );

Die optionale Funktion X_DevInit 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. Dies ist jedoch nur in der X_Ready-Funktion notwendig. In der Define-Funktion wird immer 0 (erster Verbindungsversuch) angegen

X_Undef

sub X_Undef ($$)
{
	my ( $hash, $name ) = @_;
	
	...
 
	return $error;
}

Die Undef-Funktion wird aufgerufen wenn ein Gerät mit delete gelöscht wird oder bei der Abarbeitung des Befehls rereadcfg, der ebenfalls alle Geräte löscht und danach das Konfigurationsfile neu einliest. 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 Abschnitt Pollen von Geräten).

Zugewiesene Variablen im Hash der Geräteinstanz, Internals oder Readings müssen hier nicht gelöscht werden. In fhem.pl werden die entsprechenden Strukturen beim Löschen der Geräteinstanz ohnehin vollständig gelöscht.

Beispiel:

sub X_Undef($$)    
{                     
	my ( $hash, $name) = @_;       
	DevIo_CloseDev($hash);         
	RemoveInternalTimer($hash);    
	return undef;                  
}

Sollten im Rahmen der Undef-Funktion Probleme festgestellt werden, die ein Löschen nicht zulassen, so ist als Funktionsrückgabewert eine entsprechende Fehlermeldung zurückzugeben. Nur wenn die Undef-Funktion erfolgreich durchgeführt wurde, darf undef zurückgegeben werden. Nur dann wird eine Gerätedefinition von FHEM auch tatsächlich gelöscht bzw. neu angelegt. Sollte die Undef-Funktion jedoch eine Fehlermeldung zurückgeben, wird der entsprechende Vorgang (delete bzw. rereadcfg) für dieses Gerät abgebrochen. Es bleibt dann unverändert in FHEM bestehen.

X_Delete

sub X_Delete ($$)
{
	my ( $hash, $name ) = @_;
	
	...
 
	return $error;
}

Die Delete-Funktion ist das Gegenstück zur Funktion X_Define und wird aufgerufen wenn ein Gerät mit dem Befehl delete gelöscht wird.

Wenn ein Gerät in FHEM gelöscht wird, wird zuerst die Funktion X_Undef aufgerufen um offene Verbindungen zu schließen, anschließend wird die Funktion X_Delete aufgerufen. Diese dient eher zum Aufräumen von dauerhaften Daten, welche durch das Modul evtl. für dieses Gerät spezifisch erstellt worden sind. Es geht hier also eher darum, alle Spuren sowohl im laufenden FHEM-Prozess, als auch dauerhafte Daten bspw. im physikalischen Gerät zu löschen die mit dieser Gerätedefinition zu tun haben.

Dies kann z.B. folgendes sein:

  • Löschen von Dateien im Dateisystem die während der Nutzung dieses Geräts angelegt worden sind.
  • Lösen von evtl. Pairings mit dem physikalischen Gerät

Beispiel:

sub X_Delete($$)    
{                     
	my ( $hash, $name ) = @_;       

	# Löschen von Geräte-assoziiertem Temp-File
	unlink($attr{global}{modpath}."/FHEM/FhemUtils/$name.tmp";)

	return undef;
}

Sollten im Rahmen der Delete-Funktion Probleme festgestellt werden, die ein Löschen nicht zulassen, so ist als Funktionsrückgabewert eine entsprechende Fehlermeldung zurückzugeben. Nur die Delete-Funktion erfolgreich durchgeführt wurde, darf undef zurückgegeben werden. Nur dann wird eine Gerätedefinition von FHEM auch tatsächlich gelöscht. Sollte die Delete-Funktion eine Fehlermeldung zurückgeben, wird der Löschvorgang abgebrochen und das Gerät bleibt weiter in FHEM bestehen.

X_Get

sub X_Get ($$@)
{
	my ( $hash, $name, $opt, @args ) = @_;
	
	...
 
	return $result;
}

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. In vielen Modulen wird auf diese Weise auch der Zugriff auf generierte Readings ermöglicht. Der Get-Funktion wird dabei der Geräte-Hash, der Gerätename, sowie die Aufrufparameter des get-Befehls übergeben. Als Rückgabewert wird das Ergebnis des entsprechenden Befehls in Form einer Zeichenkette zurückgegeben. Der Rückgabewert undef hat hierbei keine besondere Bedeutung und wird behandelt wie eine leere Zeichenkette "".

Beispiel:

sub X_Get($$@)
{
	my ( $hash, $name, $opt, @args ) = @_;

	return "\"get $name\" needs at least one argument" unless(defined($opt)));

	if($opt eq "status") 
	{
	   ...
	}
	elsif($opt eq "powser")
	{
	   ...
	}
	...
	else
	{
		return "Unknown argument $opt, choose one of status power [...]";
	}
}

Wenn eine unbekannte Option an die Get-Funktion übergeben wird, so muss als Rückgabewert der Funktion eine bestimmte Syntax einhalten um FHEM mitzuteilen, welche Optionen für einen Get-Befehl aktuell unterstützt werden. Die Rückgabe muss dabei folgender Syntax entsprechen:

unknown argument [Parameter] choose one of [Liste möglicher Optionen]

Hierbei sind die fett gedruckten Teile der Rückmeldung besonders wichtig. Sind diese nicht vorhanden, kann FHEM nicht die möglichen Get-Kommandos für das entsprechende Gerät ermitteln. Es muss am Anfang der Meldung das Stichwort "unknown" vorkommen gefolgt von einer frei definierbaren Fehlermeldung (i.d.R der übergebene Parameter, welcher ungültig ist). Anschließend folgt "choose one of" mit einer anschließenden Liste möglicher Optionen getrennt durch ein Leerzeichen.

Beispiel:

    return "unknown argument $opt choose one of state temperature humidity";
    

    Hier werden als mögliche Optionen für einen Get-Befehl folgende Parameter angegeben:

    • state
    • temperature
    • humidity

    Dies würde in folgenden, mögliche Get-Befehle für einen User resultieren:

    • get <NAME> state
    • get <NAME> temperature
    • get <NAME> humidity

Die Ausgabe einer solchen Meldung ist sehr 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 direkt abfragen und diesen als Return-Wert der Get-Funktion zurückgeben.


Nutzung von parseParams()

Wenn in X_Initialize $hash->{parseParams} = 1; gesetzt wurde dann wird parseParams automatisch aufgerufen und X_Get ändert sich wie folgt:

sub X_Get($$$)
{
	my ( $hash, $a, $h ) = @_;
	...

Die genauen Möglichkeiten von parseParams sind in dem entsprechenden Artikel dokumentiert.

X_Set

sub X_Set ($$@)
{
	my ( $hash, $name, $opt, @args ) = @_;
	
	...
 
	return $error;
}

Die Set-Funktion ist das Gegenteil zur Get-Funktion. Sie ist dafür gedacht, Daten zum physischen Gerät zu schicken, bzw. entsprechende Aktionen im Gerät selber auszulösen. Ein Set-Befehl dient daher der direkten Steuerung des physikalischen Gerätes in dem es bspw. Zustände verändert (wie on/off). Der Set-Funktion wird dabei der Geräte-Hash, der Gerätename, sowie die Aufrufparameter des set-Befehls übergeben. Als Rückgabewert kann eine Fehlermeldung in Form Zeichenkette zurückgegeben werden. Der Rückgabewert undef bedeutet hierbei, dass der Set-Befehl erfolgreich durchgeführt wurde. Eine Set-Funktion gibt daher nur im Fehlerfall eine Rückmeldung mit einer entsprechenden Fehlermeldung. Der Wert undef wird als "erfolgreich" interpretiert. Rückmeldungen von set-Befehlen sämtlicher Module, die im Rahmen eines ausgeführten Notify auftreten werden im FHEM Logfile festgehalten.

Falls nur interne Daten, die ausschließlich für das Modul relevant sind, gesetzt werden müssen, so sollte statt Set die Attr-Funktion verwendet werden. Attribute werden bei Save-Config auch in der Fhem.cfg gesichert. Set-Befehle nicht, da sie nur zur Steuerungszwecken im laufenden Betrieb von FHEM dienen.

Eine Set-Funktion ist ähnlich aufgebaut wie die Get-Funktion, sie bekommt jedoch in der Regel weitere zusätzliche Parameter übergeben um Zustände zu setzen.

Beispiel:

sub X_Set($@)
{
	my ( $hash, $name, $opt, @args ) = @_;

	return "\"set $name\" needs at least one argument" unless(defined($opt)));

	if($opt eq "status")
	{
	   if($args[0] eq "up")
	   {
	      ...
	   }
	   elsif($args[0] eq "down")
	   {
	      ...
	   }
	   else
	   {
	      return "Unknown value $args[0] for $opt, choose one of status power";
	   }   
	}
	elsif($opt eq "power")
	{
	   if($args[0] eq "on")
	   {
	      ...
	   }
	   elsif($args[0] eq "off")
	   {
	      ...
	   }  
	   else
	   {
	      return "Unknown value $args[0] for $opt, choose one of status power";
	   }       
	}
	...
	else
	{
		return "Unknown argument $opt, choose one of status power";
	}
}

Wenn eine unbekannte Option an die Set-Funktion übergeben wird, so muss als Rückgabewert der Funktion eine bestimmte Syntax eingehalten werden um FHEM mitzuteilen, welche Optionen für einen Set-Befehl aktuell unterstützt werden. Die Rückgabe muss dabei folgender Syntax entsprechen:

unknown argument [Parameter] choose one of [Liste möglicher Optionen]

Hierbei sind die fett gedruckten Teile der Rückmeldung besonders wichtig. Sind diese nicht vorhanden, kann FHEM nicht die möglichen Set-Kommandos für das entsprechende Gerät ermitteln. Es muss am Anfang der Meldung das Stichwort "unknown" vorkommen gefolgt von einer frei definierbaren Fehlermeldung (i.d.R der übergebene Parameter, welcher ungültig ist). Anschließend folgt "choose one of" mit einer anschließenden Liste möglicher Optionen getrennt durch ein Leerzeichen.

Beispiel:

    return "unknown argument $opt choose one of state power";
    

    Hier werden als mögliche Optionen für einen Get-Befehl folgende Parameter angegeben:

    • state
    • power

    Dies würde in folgenden, mögliche Get-Befehle für einen User resultieren:

    • get <NAME> state
    • get <NAME> power

Die Ausgabe einer solchen Meldung ist sehr wichtig, da sie im GUI-Modul verwendet wird um die möglichen set-Optionen zu ermitteln und als Auswahl anzubieten


Nutzung von parseParams()

Wenn in X_Initialize $hash->{parseParams} = 1; gesetzt wurde dann wird parseParams automatisch aufgerufen und X_Set ändert sich wie folgt:

sub X_Set($$$)
{
	my ( $hash, $a, $h ) = @_;
	...

Die genauen Möglichkeiten von parseParams sind in dem entsprechenden Artikel dokumentiert.


Nutzung von FHEMWEB-Widgets

Das GUI-Modul FHEMWEB kann für die einzelnen Set-Optionen, die das Modul versteht, automatisch Eingabehilfen wie Drop-Down Boxen oder Slider erzeugen. In der Detailansicht der GUI kann der Anwender dann die jeweiligen Werte komfortabel auswählen. Dafür muss die Set-Funktion, wenn sie mit der Option ? aufgerufen wird, nicht nur einen Text mit "Unknown ... choose one of ..." zurückgeben sondern den einzelnen Set-Optionen in diesem Rückgabetext nach einem Doppelpunkt entsprechende Zusatzinformationen anhängen. Meist prüft man in den Modulen gar nicht auf die Option ? sondern gibt generell bei unbekannten Optionen diesen Text zurück. Das Modul FHEMWEB ermittelt die Syntax eines Gerätes jedoch immer mit dem Befehl:

set <NAME> ?

Beispiel:

	return "Unknown argument $opt, choose one of state:up,down power:on,off on:noArg off:noArg";

Mit Kommata getrennte Werte ergeben eine Drop-Down Liste, mit der der User die Werte auswählen kann

timer:30,120,300
mode:verbose,ultra,relaxed

Wird kein Doppelpunkt zum Kommando angegeben, so wird eine Eingabezeile angezeigt, die die freie Eingabe eines Wertes erlaubt.

Man kann jedoch die Eingabe-/Auswahlmöglichkeiten durch Widgets vereinfachen. Dazu gibt man hinter dem Doppelpunkt einen Widgetnamen und widgetspezifische Parameter an. Es existieren mehrere solcher Widgets in FHEMWEB. Die gebräuchlichsten sind:

Zusatz Beispiel Beschreibung
noArg reset:noArg Es werden keine weiteren Argumente mehr benötigt. In so einem Fall wird bei der Auswahl keine Textbox oder ähnliches angezeigt, da keine weiteren Argumente für diesen Befehl notwendig sind.
slider:<min>,<step>,<max> dim:slider,0,1,100 Es wird ein Schieberegler angezeigt um den Parameter auszuwählen. Dabei werden als Zusatzparameter Minimum, Schrittweite und Maximum angegeben.
colorpicker rgb:colorpicker,RGB Es wird ein Colorpicker angezeigt, der dem Anwender die Auswahl einer Farbe ermöglicht. Die genaue Parametersyntax kann man dem Artikel zum Colorpicker entnehmen.
multiple group:multiple,Telefon,Multimedia,Licht,Heizung Es erscheint ein Auswahldialog, wo man verschiedene Werte durch klicken auswählen kann. Optional kann man in einem Freitext eigene Werte ergänzen. dieser Dialog wird bspw. bei der Raum-Auswahl (Attribut "room") oder der Gruppen-Auswahl (Attribut "group") in FHEMWEB genutzt.
sortable command:sortable,monday,tuesday,... Es erscheint ein Auswahldialog, wo man verschiedene Werte auswählen und sortieren kann. Man kann dabei Werte durch Klicken auswählen und durch Drag'n'Drop sortieren.

Es gibt noch weitere solcher Widgets. Eine genaue Auflistung dazu findet sich in der commandref unter widgetOverride zu FHEMWEB.

Hinweise

  • Damit in einer Eingabe bereits der aktuelle Wert vorbelegt bzw. in einer Auswahlliste der aktuelle Wert vorselektiert ist, muss es im Modul bzw. Gerät ein Reading mit dem gleichen Namen wie die Set-Option geben. Der Wert des gleichnamigen Readings wird dann als Vorbelegung / Vorselektion verwendet.
  • Der User kann sich in der Raumübersicht nach wie vor via webCmd eine entsprechende Steuerung anlegen.

X_Attr

sub X_Attr ($$$$)
{
	my ( $cmd, $name, $attrName, $attrValue  ) = @_;
	
	...
 
	return $error;
}

Die Attr-Funktion dient der Prüfung von Attributen, welche über den attr-Befehl gesetzt werden können. Sobald versucht wird, ein Attribut für ein Gerät zu setzen, wird vorher die Attr-Funktion des entsprechenden Moduls aufgerufen um zu prüfen, ob das Attribut aus Sicht des Moduls korrekt ist. Liegt ein Problem mit dem Attribut bzw. dem Wert vor, so muss die Funktion eine aussagekräftige Fehlermeldung zurückgeben, welche dem User angezeigt wird. Sofern das übergebene Attribut samt Inhalt korrekt ist, gibt die Attr-Funktion den Wert undef zurück. Erst dann wird das Attribut in der globalen Datenstruktur %attr gespeichert und ist somit erst aktiv.

Beispiel:

X_Attr(@)
{
	my ( $cmd, $name, $attrName, $attrValue ) = @_;
    
  	# $cmd  - Vorgangsart - kann die Werte "del" (löschen) oder "set" (setzen) annehmen
	# $name - Gerätename
	# $attrName/$attrValue sind Attribut-Name und Attribut-Wert
    
	if ($cmd eq "set") {
		if ($aName eq "Regex") {
			eval { qr/$aVal/ };
			if ($@) {
				Log3 $name, 3, "X ($name) - Invalid regex in attr $name $aName $aVal: $@";
				return "Invalid Regex $aVal: $@";
			}
		}
	}
	return undef;
}

Zusätzlich ist es möglich auch übergebene Attributwerte zu verändern bzw. zu korrigieren, indem man im Parameterarray @_ den ursprünglichen Wert anpasst. Dies erfolgt im Beispiel über die Modifikation des Wertes mit Index 3 (entspricht dem 4. Element) im Parameterarray, also $_[3].

Da das Attribut zum Zeitpunkt des Aufrufs der Attr-Funktion noch nicht gespeichert ist, wird der neue Wert zu diesem Zeitpunkt noch nicht via AttrVal() zurückgegeben. Erst, wenn die Attr-Funktion mit undef beendet ist, wird der neue Wert in FHEM gespeichert und steht dann via AttrVal() zur Verfügung.

Die Attr-Funktion bekommt nicht den Hash der Geräteinstanz übergeben, da sie normalerweise keine Werte dort speichern muss, sondern lediglich das Attribut auf Korrektheit prüfen muss. Im obigen Beispiel wird für ein Attribut mit Namen "Regex" geprüft ob der reguläre Ausdruck fehlerhaft ist. Sofern dieser OK ist, wird undef zurückgegeben und fhem.pl speichert den Wert des Attributs in %attr.


Attributnamen mit Platzhaltern

Falls man Attribute in der Initialize-Funktion mit Platzhaltern definiert (Wildcard-Attribute) wie z.B.:

    $hash->{AttrList} =
      "reading[0-9]*Name " .
    # usw.

dann können Anwender Attribute wie reading01Name, reading02Name etc. setzen. Leider funktioniert das bisher nicht durch Klicken in der Web-Oberfläche, da FHEMWEB nicht alle denkbaren Ausprägungen in einem Dropdown anbieten kann. Der Benutzer muss solche Attribute manuell über den attr-Befehl eingeben.

Man kann jedoch in der Attr-Funktion neu gesetzte Ausprägungen von Wildcard-Attributen an die gerätespezifische userattr-Variable anfügen. Dann können bereits gesetzte Attribute in FHEMWEB durch Klicken ausgewählt und geändert werden. Dazu reicht ein Aufruf der Funktion addToDevAttrList():

    addToDevAttrList($name, $aName);

X_Read

sub X_Read ($)
{
	my ( $hash ) = @_;
	
	...
}

Die X_Read-Funktion wird aufgerufen, wenn ein dem Gerät zugeordneter Filedeskriptor (serielle Schnittstelle, TCP-Verbindung, ...) Daten zum Lesen bereitgestellt hat. Die Daten müssen nun eingelesen und interpretiert werden.

Im folgenden Beispiel wird über eine serielle Schnittstelle (beziehungsweise über einen USB-To-Seriell-Konverter) von einem angeschlossenen Gerät gelesen. Dazu werden die bisher verfügbaren Daten mit der Funktion DevIo_SimpleRead gelesen. Da die Übertragung möglicherweise noch nicht vollständig ist, kann es sein, dass kurz darauf die X_Read-Funktion wieder aufgerufen wird und ein weiterer Teil oder der Rest der Daten gelesen werden kann. Die Funktion muss daher prüfen ob schon alle erwarteten Daten angekommen sind und gegebenenfalls die bisher gelesenen Daten in einem eigenen Puffer (idealerweise in $hash) zwischenspeichern. Im Beispiel ist dies $hash->{helper}{BUFFER} an den die aktuell gelesenen Daten angehängt werden, bis die folgende Prüfung ein für das jeweilige Protokoll vollständige Frame erkennt.

sub X_Read($)
{
	my ($hash) = @_;
	my $name = $hash->{NAME};
	
	# einlesen der bereitstehenden Daten
	my $buf = DevIo_SimpleRead($hash);		
	return "" if ( !defined($buf) );
	Log3 $name, 5, "X ($name) - received data: ".$buf;    

	# Daten in Hex konvertiern und an den Puffer anhängen
	$hash->{helper}{BUFFER} .= unpack ('H*', $buf);	
	Log3 $name, 5, "X ($name) - current buffer content: ".$hash->{helper}{BUFFER};

	# prüfen, ob im Frame ein vollständiger Frame zur Verarbeitung vorhanden ist.
	if ($hash->{buffer} =~ "ff1002(.{4})(.*)1003(.{4})ff(.*)") {
	...

Die zu lesenden Nutzdaten können dann je nach Protokoll des Geräts beispielsweise an einer festgelegten Stelle im Frame (dann in $hash->{helper}{BUFFER}) stehen oder aus dem Kontext mit einem Regex-Match extrahiert werden und via Reading-Funktionen in Readings gespeichert werden (siehe unten).

Der Rückgabewert der Read-Funktion wird nicht geprüft und hat daher keinerlei Bedeutung.

X_Ready

sub X_Ready ($)
{
	my ( $hash ) = @_;
	
	...
    
	return $success;
}

Wird im Main-Loop aufgerufen falls das Modul in der globalen Liste %readyfnlist existiert. Diese Funktion hat je nachdem auf welchem OS FHEM ausgeführt wird unterschiedliche Aufgaben:

  • UNIX-artiges Betriebssystem: prüfen, ob eine Verbindung nach einem Verbindungsabbruch wieder aufgebaut werden kann. Sobald der Verbindungsaufbau erfolgreich war, muss die Funktion einen erfolgreichen Wahrheitswert zurückliefern (z.B. "1") und den eigenen Eintrag entsprechend aus %readyfnlist löschen.
  • Windows-Betriebssystem: prüfen, ob lesbare Daten für ein serielles Device (via COM1, COM2, ...) vorliegen. Sofern lesbare Daten vorliegen, muss Funktion einen erfolgreichen Wahrheitswert zurückliefern (z.B. "1"). Zusätzlich dazu muss die Funktion, wie bei UNIX-artigen Betriebssystem, ebenfalls bei einem Verbindungsabbruch einen neuen Verbindungsversuch initiieren. Der Eintrag in %readyfnlist bleibt solange erhalten, bis die Verbindung seitens FHEM beendet wird.

Der Windows-spezifische Teil zur Datenprüfung ist dabei nur zu implementieren, wenn das Modul über eine serielle Verbindung kommuniziert.

Bei der Nutzung des Moduls DevIo wird dem Modulentwickler der Umgang mit %readyfnlist abgenommen, da DevIo sich selbst um die entsprechenden Einträge kümmert und diese selbstständig wieder entfernt.

In der Regel sieht eine Ready-Funktion immer gleich aus.

Beispiel:

sub X_Ready($)
{
	my ($hash) = @_;
      
	# Versuch eines Verbindungsaufbaus, sofern die Verbindung beendet ist.
	return DevIo_OpenDev($hash, 1, undef ) if ( $hash->{STATE} eq "disconnected" );

	# This is relevant for Windows/USB only
	if(defined($hash->{USBDev})) {
		my $po = $hash->{USBDev};
		my ( $BlockingFlags, $InBytes, $OutBytes, $ErrorFlags ) = $po->status;
		return ( $InBytes > 0 );
	}
}

X_Notify

Die X_Notify-Funktion wird aus der Funktion DoTrigger() in fhem.pl heraus aufgerufen sobald ein Modul Events erzeugt hat. Damit kann ein Modul auf Events anderer Module reagieren. Typische Beispiele sind dabei das FileLog-Modul oder das notify-Modul.

Die Notify-Funktion bekommt dafür zwei Hash-Referenzen übergeben: den Hash des eigenen Geräts und den Hash des Geräts, dass die Events erzeugt hat. Über den Hash des eigenen Geräts kann die Notify-Funktion beispielsweise auf die Internals oder Attribute des eigenen Geräts zugreifen. Über den Hash des Gerätes und der deviceEvents()-Funktion kann man auf die generierten Events zugreifen. Über den zweiten Parameter dieser Routine lässt sich bestimmen ob für das Reading state ein 'normales' Event (d.h. in der form state: <wert>) erzeugen soll (Wert: 1) oder ob z.b. aus Gründen der Rückwärtskompatibilität ein Event ohne state: erzeugt werden soll. Falls dem Anwender die Wahl des verwendeten Formats überlassen werden soll ist hierzu das addStateEvent-Attribut vorzusehen.

Der direkte Zugriff auf $hash->{CHANGED} ist nicht mehr zu empfehlen.

Beispiel:

sub X_Notify($$)
{
  my ($own_hash, $dev_hash) = @_;
  my $ownName = $own_hash->{NAME}; # own name / hash

  return "" if(IsDisabled($ownName)); # Return without any further action if the module is disabled

  my $devName = $dev_hash->{NAME}; # Device that created the events

  my $events = deviceEvents($dev_hash,1);
  return if( !$events );

  foreach my $event (@{$events}) {
    $event = "" if(!defined($event));

    # Examples:
    # $event = "readingname: value" 
    # or
    # $event = "INITIALIZED" (for $devName equal "global")
    #
    # processing $event with further code
  }
}


Begrenzung der Aufrufe auf bestimmte Geräte

Da die Notify-Funktion für jedes definierte Gerät mit all seinen Events aufgerufen wird, muss sie in einer Schleife jedesmal prüfen und entscheiden, ob es mit dem jeweiligen Event etwas anfangen kann. Ein Gerät, dass die Notify-Funktion implementiert, sieht dafür typischerweise einen regulären Ausdruck vor, welcher für die Filterung verwendet wird.

Wenn man nur gezielt von bestimmten Definitionen Events erhalten will, kann man diese auch in Form einer devspec in $hash->{NOTIFYDEV} angeben. Bspw. kann man in der Define-Funktion diesen Wert setzen. Dadurch wird die Notify-Funktion nur aufgerufen wenn eine der Definitionen, auf welche die devspec passt, ein Event erzeugt hat. Ein typischer Fall ist die Begrenzung von Events auf "global":

in der Define-Funktion:

$hash->{NOTIFYDEV} = "global";
$hash->{NOTIFYDEV} = "global,Definition_A,Definition_B";
$hash->{NOTIFYDEV} = "global,TYPE=CUL_HM";

Dies schont insbesondere bei grossen Installationen Ressourcen, da die Notify-Funktion nicht sämtliche Events, sondern nur noch Events der gewünschten Definitionen erhält. Dadurch erfolgen deutlich weniger Aufrufe der Notify-Funktion, was Systemressourcen schont.


Reihenfolge für den Aufruf der Notify-Funktion beeinflussen

Sobald ein Event ausgelöst wurde, stellt sich FHEM eine Liste aller relevanten Geräte-Hashes zusammen, welche via Notify-Funktion prüfen müssen, ob das Event relevant ist. Dabei wird die Liste nach $hash->{NTFY_ORDER} sortiert. Diese enthält ein Order-Präfix in Form einer Ganzzahl, sowie den Namen der Definition (Bsp: 50-Lampe_Wohnzimmer). Dadurch kann man jedoch nicht sicherstellen, dass Events von bestimmten Modulen zuerst verarbeitet werden.

Wenn das eigene Modul bei der Eventverarbeitung gegenüber den anderen Modulen eine bestimmte Reihenfolge einhalten muss, kann man in der Initialize-Funktion durch Setzen von $hash->{NotifyOrderPrefix} diese Reihenfolge beeinflussen. Standardmäßig werden Module immer mit einem Order-Präfix von "50-" in FHEM registriert. Durch die Veränderung dieses Präfixes kann man das eigene Modul in der Reihenfolge gegenüber anderen Modulen bei der Eventverarbeitung beeinflussen.

Beispiel:

sub X_Initialize($)
{
	my ($hash) = @_;
	
	...
	
	$hash->{NotifyOrderPrefix} = "45-"  # Alle Definitionen des Moduls X werden bei der Eventverarbeitung zuerst geprüft
	
	# oder...
	
	$hash->{NotifyOrderPrefix} = "55-"  # Alle Definitionen des Moduls X werden bei der Eventverarbeitung als letztes geprüft

Da dieses Präfix bei eventverarbeitenden Definitionen in $hash->{NTFY_ORDER} dem Definitionsnamen vorangestellt wird bewirkt es bei einer normalen aufsteigenden Sortierung nach $hash->{NTFY_ORDER} eine veränderte Reihenfolge. Alle Module die in der Initialize-Funktion nicht $hash->{NotifyOrderPrefix} explizit setzen, werden mit "50-" als Standardwert vorbelegt.

X_DbLog_splitFn

sub X_DbLog_split ($$)
{
	my ( $event, $device_name ) = @_;
	
	...
    
	return  ( $reading, $value, $unit );
}

Die DbLog_split-Funktion wird durch das Modul DbLog aufgerufen, sofern der Nutzer DbLog benutzt. Sofern diese Funktion implementiert ist, kann der Modulautor das Auftrennen von Events in den Reading-Namen, -Wert und der Einheit selbst steuern. Andernfalls nimmt DbLog diese Auftrennung selber mittels Trennung durch Leerzeichen sowie vordefinierten Regeln zu verschiedenen Modulen vor. Je nachdem, welche Readings man in seinem Modul implementiert, passt diese standardmäßige Trennung jedoch nicht immer.

Der Funktion werden folgende Eingangsparameter übergeben:

  1. Das generierte Event (Bsp: temperature: 20.5 °C)
  2. Der Name des Geräts, welche das Event erzeugt hat (Bsp: Temperatursensor_Wohnzimmer)

Es ist nicht möglich in der DbLog_split-Funktion auf die verarbeitende DbLog-Definition zu referenzieren.

Als Rückgabewerte muss die Funktion folgende Werte bereitstellen:

  1. Name des Readings (Bsp: temperature)
  2. Wert des Readings (Bsp: 20.5)
  3. Einheit des Readings (Bsp: °C)

Beispiel:

sub X_DbLog_splitFn($$)
{
	my ($event, $device) = @_;
	my ($reading, $value, $unit);
        my $devhash = $defs{$device}

	if($event =~ m/temperature/) {
	   $reading = 'temperature';
	   $value = substr($event,12,4);
	   $unit = '°C';
	}   
        
        return ($reading, $value, $unit);
}

X_Rename

sub X_Rename ($$)
{
	my ( $new_name, $old_name) = @_;
	
	...
}

Die Rename-Funktion wird ausgeführt, nachdem ein Gerät umbenannt wurde. Auf diese Weise kann ein Modul auf eine Namensänderung reagieren, wenn das Gerät $old_name in $new_name umbenannt wurde. Ein typischer Fall ist das Umsetzen der Namensänderungen bei Daten die mittels setKeyValue() gespeichert wurden. Hierbei müssen die Daten, welche unter dem alten Namen gespeichert sind, auf den neuen Namen geändert werden.

Der Rename-Funktion wird lediglich der alte, sowie der neue Gerätename übergeben. Der Rückgabewert wird nicht ausgewertet.

Beispiel:

sub X_Rename ($)
{
	my ( $new_name, $old_name ) = @_;

	my $old_index = "Module_X_".$old_name."_data";
	my $new_index = "Module_X_".$new_name."_data";

	my ($err, $data) = getKeyValue($old_index);
	return undef unless(defined($old_pwd));

	setKeyValue($new_index, $data);
	setKeyValue($old_index, undef);
}

X_Shutdown

sub X_Shutdown ($)
{
	my ( $hash ) = @_;
	
	...
}

Mit der X_Shutdown Funktion kann ein Modul Aktionen durchführen bevor FHEM gestoppt wird. Dies kann z.B. der ordnungsgemäße Verbindungsabbau mit dem physikalischen Gerät sein (z.B. Session beenden, Logout, etc.). Als Übergabeparameter wird der Geräte-Hash bereitgestellt. Der Rückgabewert einer Shutdown-Funktion wird nicht ausgewertet und ist daher irrelevant.

Beispiel:

sub X_Shutdown($)
{
	my ($hash) = @_;

	# Verbindung schließen
	DevIo_CloseDev($hash);
        return undef;
}

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 um einen Funktionsaufruf zu einem späteren Zeitpunkt durchführen zu können. Man übergibt dabei den Zeitpunkt für den nächsten Aufruf, den Namen der Funktion, die aufgerufen werden soll, sowie den zu übergebenden Parameter. Als zu übergebender Parameter wird üblicherweise der Hash der betroffenen Geräteinstanz verwendet. Damit hat die aufgerufene Funktion Zugriff auf alle wichtigen Daten der Geräteinstanz. Eventuell zusätzlich benötigte Werte können einfach als weitere Internals über den Hash zugänglich gemacht werden.

Beispielsweise könnte man für das Abfragen eines Geräts in der Define-Funktion den Timer folgendermaßen setzen:

# initial request after 2 secs, there timer is set to interval for further update
InternalTimer(gettimeofday()+2, "X_GetUpdate", $hash);

Alternativ kann man auch in der Notify-Funktion auf das Event global:INITIALIZED bzw. global:REREADCFG reagieren und erst dort, den Timer anstoßen, sobald die Konfiguration komplett eingelesen wurde. Dies ist insbesondere notwendig, wenn man sicherstellen will, dass alle Attribute aus der Konfiguration gesetzt sind, sobald man einen Status-Update initiiert.

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};
	Log3 $name, 4, "X: GetUpdate called ...";
	
	...
	
	InternalTimer(gettimeofday()+$hash->{Interval}, "X_GetUpdate", $hash);
}

Innerhalb der Funktion kann man nun das Gerät abfragen und die abgefragten Werte in Readings speichern. Falls das Abfragen der Werte jedoch zu einer Verzögerung und damit zu einer Blockade von FHEM führen kann, ist es möglich, in der GetUpdate-Funktion nur die Aufforderung zum Senden bestimmter Daten an das angeschlossene Gerät zu senden und dann das Lesen über die oben beschriebene Read-Funktion zu implementieren.

Eine genaue Beschreibung der Timer-Funktion gibt es hier im Wiki

Logging / Debugging

Um Innerhalb eines Moduls eine Log-Meldung in die FHEM-Logdatei zu schreiben, wird die Funktion Log3() aufgerufen.

Log3 $name, 3, "X ($name) - Problem erkannt ...";

Eine genaue Beschreibung zu der Funktion inkl. Aufrufparameter findet man hier. Es ist generell ratsam in der Logmeldung sowohl den Namen des eigenen Moduls zu schreiben, sowie den Namen des Geräts, welche diese Logmeldung produziert, da die Meldung, so wie sie ist, direkt in das Logfile wandert und es für User ohne diese Informationen schwierig ist, die Meldungen korrekt zuzuordnen.

Die Funktion Log3() verwendet den Namen der Geräteinstanz um das verbose-Attribut zu prüfen. In der Regel wird bei Modulfunktionen jedoch immer nur der Gerätehash $hash übergeben. Um den Namen der Definition zu ermitteln ist es daher notwendig sich diesen aus dem Hash extrahieren:

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

Logmeldungen sollten je nach Art und Wichtigkeit für den Nutzer in unterschiedlichen Loglevels erzeugt werden. Es gibt insgesamt 5 Stufen in denen geloggt werden kann. Standardmäßig steht der systemweite Loglevel (global-Attribut verbose) auf der Stufe 3. Die Bedeutung der jeweiligen Stufen ist in der commandref beschrieben.

Während der Entwicklung eines Moduls kann man für eigene Debug-Zwecke auch die Funktion Debug() verwenden um schnell und einfach Debug-Ausgaben in das Log zu schreiben. Diese sollten in der endgültigen Fassung jedoch nicht mehr vorhanden sein. Sie dienen ausschließlich zum Debugging während der Entwicklung.

Eine genaue Beschreibung der Log-Funktion gibt es hier im Wiki.

Zweistufiges Modell für Module

Zweistufiges Modulkonzept.jpg

Es gibt viele Geräte, welche die Kommunikation mit weiteren Geräten mit tlw. unterschiedlichen Protokollen ermöglichen. Das typischste Beispiel bietet hier der CUL, welcher via Funk mit verschiedenen Protokollen weitere Geräte ansprechen kann (z.B. Aktoren, Sensoren, ...). Hier bildet ein Gerät eine Brücke durch die weitere Geräte in FHEM zugänglich gemacht werden können. Dabei werden über einen Kommunikationsweg (z.B. serielle Schnittstelle, TCP, ...) beliebig viele Geräte gesteuert. Typische Beispiele dazu sind:

  • CUL: stellt Geräte mit verschiedenen Kommunikationsprotokollen via Funk bereit (u.a. FS20, HomeMatic, FHT, MAX, ...)
  • HMLAN: stellt HomeMatic Geräte via Funk bereit
  • MAXLAN: stellt MAX! Geräte via Funk bereit
  • panStamp: stellt weitere panStamp Geräte via Funk bereit

Dabei wird die Kommunikation in 2 Stufen unterteilt:

  • physisches Modul - z.B. 00_CUL.pm - zuständig für die physikalische Kommunikation mit der Hardware. Empfangene Daten müssen einem logischen Modul zugeordnet werden.
  • logische Modul(e) - z.B. 10_FS20.pm - interpretiert protokollspezifische Nachrichten. Sendet protokollspezifische Daten über das physische Modul an die Hardware.

physisches Modul

Das physische Modul öffnet die Datenverbindung zum Gerät (z.B. CUL) und verarbeitet sämtliche Daten. Es kümmert sich um den Erhalt der Verbindung (bsp. durch Keep-Alives) und konfiguriert das Gerät so, dass eine Kommunikation mit allen weiteren Geräten möglich ist (bsp. Frequenz, Modulation, Kanal, etc.).

Empfangene Nutzdaten werden über die Funktion Dispatch() an logische Module weitergegeben.

Das Modul stellt eine Match-Liste bereit, anhand FHEM die Nachricht einem Modul zuordnen kann, sofern dieses noch nicht geladen sein sollte. FHEM lädt dann automatisch dieses Modul zwecks Verarbeitung der Nachricht. Das Modul stellt eine Write-Funktion zur Verfügung, über die logische Module Daten zum Gerät übertragen können. Anhand einer bereitgestellten Client-Liste kann FHEM feststellen, welche logischen Module mit diesem Modul kommunizieren können.

logisches Modul

Das logische Modul interpretiert die Nachricht und erzeugt entsprechende Readings/Events. Es stellt über set-/get-Kommandos Steuerungsmöglichkeiten dem Nutzer zur Verfügung.

Es stellt FHEM einen regulären Ausdruck zur Verfügung anhand FHEM ermitteln kann, ob die Nachricht durch das logische Modul verarbeitet werden kann.

Benötigte Modulfunktionen

Für das zweistufige Modulkonzept muss in einem logischen Modul eine Parse-Funktion im Modul-Hash registriert werden. In einem physikalischen Modul muss eine Write-Funktion definiert sein. Diese dienem dem Datenaustausch in beide Richtungen und werden von dem jeweils anderen Modul indirekt aufgerufen.

Diese Funktionen werden wie folgt implementiert:

X_Parse

{{Randnotiz|RNTyp=Info|RNText=

Info green.pngACHTUNG: Dieser Abschnitt geht davon aus, dass das Modul mit dem Namen "X" ein logisches Modul im Sinne des zweistufigen Modulkonzepts ist, also Daten mit einem übergeordneten, physikalischen Modul austauscht.
sub X_Parse ($$)
{
	my ( $io_hash, $message) = @_;
	
	...
	
	return $found;
}

Die Funktion X_Parse wird aufgerufen, sobald von dem IO-Gerät $io_hash eine Nachricht $message via Dispatch() zur Verarbeitung angefragt wird. Die Parse-Funktion muss dann prüfen, zu welcher Gerätedefinition diese Nachricht gehört und diese entsprechend verarbeiten.

Üblicherweise enthält eine Nachricht immer eine Komponente durch welche sich die Nachricht einem Gerät zuordnen lässt (z.B. Adresse, ID-Nummer, ...). Eine solche Identifikation sollte man im Rahmen der Define-Funktion im logischen Modul an geeigneter Stelle speichern, um in der Parse-Funktion eine einfache Zuordnung von Adresse/ID einer Nachricht zur entsprechenden Gerätedefinition zu haben. Dazu wird in der Regel im Modul-Hash im modulspezifischen Berreich eine Liste defptr (Definition Pointer) geführt, welche jede eindeutige Adresse/ID dem entsprechenden Geräte-Hash zuordnet:

sub X_Define ($$)
{
	my ( $hash, $def) = @_;
	my @a = split("[ \t][ \t]*", $def);
	my $name = $a[0];

	...
	
	# erstes Argument ist die eindeutige Geräteadresse
	my $address = $a[1];

	# Adresse rückwärts dem Hash zuordnen (für ParseFn)
	$modules{X}{defptr}{$address} = $hash;

	...
}

Auf Basis dieses Definition Pointers kann die Parse-Funktion nun sehr einfach prüfen, ob für die empfangene Nachricht bereits eine entsprechende Gerätedefinition existiert. Sofern diese existiert, kann die Nachricht entsprechend verarbeitet werden. Sollte jedoch keine passende Gerätedefinition zu der empfangenen Nachricht existieren, so muss die Parse-Funktion den Gerätenamen "UNDEFINED" zusammen mit den Argumenten für einen define-Befehl zurückgeben, welcher ein passendes Gerät in FHEM anlegen würde (durch autocreate).

Beispiel:

sub X_Parse ($$)
{
	my ( $io_hash, $message) = @_;
	
	# Die Stellen 10-15 enthalten die eindeutige Identifikation des Geräts
	my $address = substr($message, 10, 5); 

	# wenn bereits eine Gerätedefinition existiert (via Definition Pointer aus Define-Funktion)
	if(my $hash = $modules{X}{defptr}{$address}) 
	{
		...  # Nachricht für $hash verarbeiten
		
		# Rückgabe des Gerätenamens, für welches die Nachricht bestimmt ist.
		return $hash->{NAME}; 
	}
	else
	{
		# Keine Gerätedefinition verfügbar
		# Daher Vorschlag define-Befehl: <NAME> <MODULNAME> <ADDRESSE>
		return "UNDEFINED X_".$address." X $address";
	}
}

X_Write

Info green.pngACHTUNG: Dieser Abschnitt geht davon aus, dass das Modul mit dem Namen "X" ein physisches Modul im Sinne des zweistufigen Modulkonzepts ist, also Daten mit untergeordneten logischen Modulen austauscht.
sub X_Write ($$)
{
	my ( $hash, @arguments) = @_;
	
	...
	
	return $return;
}

Die Write-Funktion wird durch die Funktion IOWrite() aufgerufen, sobald eine logische Gerätedefinition Daten per IO-Gerät an die Hardware übertragen möchte. Dazu kümmert sich die Write-Funktion um die Übertragung der Nachricht in geeigneter Form an die verbundene Hardware. Als Argumente wird der Hash des physischen Gerätes übertragen, sowie alle weiteren Argumente, die das logische Modul beim Aufruf von IOWrite() mitgegeben hat. Im Normalfall ist das ein Skalar mit der zu sendenden Nachricht in Textform. Es kann aber auch sein, dass weitere Daten zum Versand notwendig sind (evtl. Schlüssel, Session-Key, ...). Daher ist Parametersyntax einer zu schreibenden Nachricht via IOWrite-/Write-Funktion zwischen logischem und physikalischen Modul abzustimmen.

Beispiel:

sub X_Write ($$)
{
	my ( $hash, $message, $address) = @_;
	
	DevIo_SimpleWrite($hash, $address.$message, 2);

	return undef;
}

Die Client-Liste

Die Client-Liste ist eine Auflistung von Modulnamen (genauer: regulären Ausdrücken die auf Modulnamen passen) die in einem physischen Modul gesetzt ist. Damit wird definiert, mit welche logischen Modulen das physikalische Modul kommunizieren kann.

Eine Client-Liste ist eine Zeichenkette, welche aus allen logischen Modulnamen besteht. Die einzelnen Namen werden durch einen Doppelpunkt getrennt. Anstatt kompletter Modulnamen können auch reguläre Ausdrücke verwendet werden, die auf mehrere Modulnamen passen.

Bsp.: Die Client-Liste von dem Modul CUL lautet daher wie folgt:

FS20:FHT.*:KS300:USF1000:BS:HMS:CUL_EM:CUL_WS:CUL_FHTTK:CUL_HOERMANN:ESA2000:CUL_IR:CUL_TX:Revolt:IT:UNIRoll:SOMFY:STACKABLE_CC:CUL_RFR:CUL_TCM97001:CUL_REDIRECT

Alle hier aufgelisteten Module können über das Modul CUL Daten empfangen bzw. senden.

Die Client-Liste hat generell folgende Funktion:

  • Die Funktion Dispatch() prüft nur Module, welche in der Client-Liste enthalten sind, ob diese die Nachricht verarbeiten können.
  • Die Funktion AssignIoPort() prüft anhand sämtlicher Client-Listen in FHEM, welches IO-Gerät für ein logisches Gerät nutzbar ist.

Üblicherweise wird die Client-Liste in der Initialize-Funktion im Modul-Hash gesetzt:

sub X_Initialize($)
{
	my ($hash) = @_;
	...
	$hash->{Clients} = "FS20:KS300:FHT.*";
}

Man kann die Client-Liste jedoch auch pro physikalisches Gerät setzen. Eine gesetzte Client-Liste in einem Gerät hat immer Vorrang vor der Liste im Modul-Hash. Eine gerätespezifische Client-Liste wird dann verwendet, wenn bspw. ein Gerät je nach Konfiguration nur bestimmte logische Module bedienen kann. Bspw. kann ein CUL je nach RF-Einstellungen FS20, uvm. oder nur HomeMatic bedienen. In einem solchen Fall wird die Client-Liste im Geräte-Hash gesetzt:

sub X_Define($$)
{
	my ( $hash, $def ) = @_;
	...
	$hash->{Clients} = "CUL_HM";
}

In vielen Modulen, welche nach dem zweistufigem Konzept arbeiten, beginnt und endet die Client-Liste mit einem Doppelpunkt. Dies ist ein historisches Überbleibsel, da der Prüfmechanismus die Client-Liste früher auf das Vorhandensein von :<Modulname>: prüfte. Dies ist nun nicht mehr notwendig. Die einzelnen Modulnamen müssen lediglich durch einen Doppelpunkt getrennt werden.

Die Match-Liste

Info green.pngACHTUNG:

Sämtliche regulären Ausdrücke in der Match-Liste werden "case insensitive" überprüft. Das bedeutet, dass Groß-/Kleinschreibung nicht berücksichtigt wird.

Um dennoch in einem regulären Ausdruck auf Groß-/Kleinschreibung zu prüfen, kann man dieses Verhalten mit dem Modifizierer (?-i) wieder abschalten:

my %matchListFHEMduino = (
    ....
    "5:FHEMduino_PT2262"   => "^(?-i)IR.*\$",
    ....
    "13:IT"                => "^(?-i)i......\$",
);
Siehe dazu Forumsbeitrag: Thema

Die Match-Liste ordnet eine Nachrichtensyntax (regulärer Ausdruck) einem Modulnamen zu. Sollte eine Nachricht vom physikalischen Gerät empfangen werden, die durch kein geladenes Modul verarbeitet werden kann, so wird über die Match-Liste geprüft, welches Modul diese Nachricht verarbeiten kann. Dieses Modul wird anschließend geladen und die Nachricht durch dieses verarbeitet. In dieser Liste findet mittels regulärem Ausdruck eine Zuordnung der Nachrichtenstruktur zum verarbeitenden logischen Modul statt.

Diese Liste wird ausschließlich in der Dispatch()-Funktion verwendet. Sollte keine passendes Modul, welches bereits geladen ist, zur Verarbeitung einer Nachricht gefunden werden, so wird mithilfe der Match-Liste aufgrund der vorliegenden Nachricht das entsprechende Modul ermittelt. Dieses Modul wird dann direkt geladen und die Nachricht wird via Parse-Funktion verarbeitet.

Die Match-Liste ist eine Zuordnung von einem Sortierpräfix + Modulname zu einem regulären Ausdruck:

{
    "1:FS20"  => "^81..(04|0c)..0101a001",
    "2:KS300" => "^810d04..4027a001",
    "3:FHT"   => "^81..(04|09|0d)..(0909a001|83098301|c409c401).."
}

Das Sortierpräfix dient als Sortierhilfe um so die Reihenfolge der Prüfung festzulegen. Bei der Prüfung wird der Hash mittels sort() sortiert und die regulären Ausdrücke werden dann nacheinander getestet. Daher sollten die präzisesten Ausdrücke immer zuerst getestet werden, sofern es weniger präzise Ausdrücke in der Match-Liste gibt. Dabei ist zu beachten, dass der Sortierpräfix nicht nach numerischen Regeln sortiert wird, sondern basierend auf der Zeichenreihenfolge.

Üblicherweise wird die Match-Liste in der Initialize-Funktion im Modul-Hash gesetzt:

sub X_Initialize($)
{
	my ($hash) = @_;

	...

	$hash->{MatchList} = { "1:FS20"      => "^81..(04|0c)..0101a001",
			       "2:KS300"     => "^810d04..4027a001",
			       "3:FHT"       => "^81..(04|09|0d)..(0909a001|83098301|c409c401).." };
}

Man kann die Match-Liste, ähnlich wie bei der Client-Liste, auch pro physikalisches Gerät setzen. Dabei hat auch hier die Match-Liste eines Gerätes immer Vorrang vor der Match-Liste aus dem Modul-Hash:

sub X_Define($$)
{
	my ($hash, $def) = @_;

	...

	$hash->{MatchList} = { "1:CUL_HM" => "^A...................." };
}

Kommunikation vom Gerät zu den logischen Modulen

Die X_Read-Funktion wird aus der Hauptschleife von fhem.pl aufgerufen sobald das Gerät, für welche das Modul zuständig ist, Daten zum Lesen bereit gestellt hat.

Unter Windows funktioniert die Prüfung via select() nur für Geräte, die via TCP verbunden sind. Für alle anderen Verbindungsarten (z.B. seriell) ist eine X_Ready-Funktion von Nöten, die 10x pro Sekunde das Gerät abfrägt und "true" zurück gibt, sollten Daten bereit stehen.

Die X_Read-Funktion stellt sicher, dass die Daten

  • komplett (in der Regel über einen internen Puffer in $hash) und
  • korrekt (z.B. via Regex)

sind und ruft die globale Funktion Dispatch() mit einer kompletten Nachricht auf.

Dispatch() sucht nach einer passenden Definition via $hash->{Clients} in physischen Definitionen und $hash->{Match} in allen passenden logischen Definitionen und ruft X_Parse in den gefundenen Modulen auf. Sofern keine passende Defintion gefunden wurde um die Nachricht zu verarbeiten, wird in der MatchList des Moduls der physischen Definition gesucht, welche bei der Initialisierung des Moduls via X_Initialize übergeben wurde ($hash->{MatchList}). Sollte es darin ein Modul geben, was diese Art von Nachricht verarbeiten kann, so wird versucht dieses Modul zu laden und eine neue Definition via Autocreate-Mechanismus anzulegen.

X_Parse

  • interpretiert die übergebene Nachricht
  • setzt alle Readings via readings*update()-Funktionen
  • gibt den Namen der logischen Definition zurück, welches die Nachricht verarbeitet hat

Es findet während der Verarbeitung einer Nachricht durch Dispatch() keine sofortige Eventverarbeitung statt, wenn die readings*update Funktionen innerhalb von X_Parse aufgerufen werden. (Im Gegensatz zum direkten Aufrufen der readings*update Funktionen ohne vorhergehendes Dispatch() )

Dispatch() triggert das Event-Handling für das von X_Parse zurückgegebene logische Device selbstständig.

Kommunikation von den logischen Modulen zum Gerät

Um von einem logischen Modul an ein physisches Gerät zu senden, wird im logischen Modul das Attribut IODev mit dem namen des physischen Devices gesetzt. Der Befehl AssignIoPort($hash); in der X_Define-Funktion des logischen Devices erledigt das.

Als Befehl zum Schreiben vom logischen ins physische Gerät soll IOWrite() verwendet werden. IOWrite() ruft im physischen Gerät die X_Write-Funktion auf.

Wenn es keine direkte Kommunikation zwischen dem logischen und dem physischen Gerät gibt(keine direkten Aufrufe von Funktionen, kein direktes überprüfen von $hash Werten,...) so können die Module hintereinander geschaltet werden (z.B. für Routerfunktionen wie in RFR) oder mittels FHEM2FHEM:RAW zwei Fhem Installationen verbunden werden und die logischen Devices werden dennoch funktionieren.

Ergänzende Hinweise

Die Wahl der vorangestellten Nummer für den Dateinamen eines neuen Moduls hat keine Bedeutung mehr, es sei denn die Nummer ist 99. Module, die mit 99_ beginnen, werden von FHEM automatisch geladen. Module mit einer anderen Nummer nur wenn ein define-Befehl dafür sorgt, dass das Modul geladen wird.

Wenn ein Modul Initialisierungsdaten benötigt, sollten diese im Modul selbst enthalten sein. Eine zusätzliche Datei oder sogar ein Unterverzeichnis mit mehreren Dateien ist bei FHEM nicht üblich und sollte bei Modulen, die mit FHEM ausgeliefert werden nur in Rücksprache mit Rudolf König angelegt werden, da sie sonst bei einem Update nicht verteilt werden.

Weitere Informationen

Wenn man weitere Details wissen möchte, ist ein erster sinnvoller Schritt ein Blick in die Datei fhem.pl. Dort sieht man im Perl-Code wie die Module aufgerufen werden, was vorher passiert und was danach. Am Anfang der Datei (ca. ab Zeile 130) findet man beispielsweise eine Liste der globalen Variablen, die den Modulen zur Verfügung stehen sowie Details zu den wichtigen Hashes %modules und %defs. Wer mit Perl noch nicht so gut klar kommt, dem hilft eventuell ein Blick auf die Perldoc Website[1] oder in das Perl-Buch seiner Wahl. Auch die FHEM Commandref [2] sollte nicht unterschätzt werden. Es stehen oft mehr interessante Details auch für Modulentwickler darin als man zunächst vermuten könnte.


"Hello World" Beispiel

98_Hello.pm

package main;
use strict;
use warnings;

my %Hello_gets = (
	"whatyouwant"	=> "can't",
	"whatyouneed"	=> "try sometimes",
	"satisfaction"  => "no"
);

sub Hello_Initialize($) {
    my ($hash) = @_;

    $hash->{DefFn}      = 'Hello_Define';
    $hash->{UndefFn}    = 'Hello_Undef';
    $hash->{SetFn}      = 'Hello_Set';
    $hash->{GetFn}      = 'Hello_Get';
    $hash->{AttrFn}     = 'Hello_Attr';
    $hash->{ReadFn}     = 'Hello_Read';

    $hash->{AttrList} =
          "formal:yes,no "
        . $readingFnAttributes;
}

sub Hello_Define($$) {
    my ($hash, $def) = @_;
    my @param = split('[ \t]+', $def);
    
    if(int(@param) < 3) {
        return "too few parameters: define <name> Hello <greet>";
    }
    
    my $hash->{name}  = $param[0];
    my $hash->{greet} = $param[2];
    
    return undef;
}

sub Hello_Undef($$) {
    my ($hash, $arg) = @_; 
    # nothing to do
    return undef;
}

sub Hello_Get($@) {
	my ($hash, @param) = @_;
	
	return '"get Hello" needs at least one argument' if (int(@param) < 2);
	
	my $name = shift @param;
	my $opt = shift @param;
	if(!$Hello_gets{$opt}) {
		my @cList = keys %Hello_gets;
		return "Unknown argument $opt, choose one of " . join(" ", @cList);
	}
	
	if($attr{$name}{formal} eq 'yes') {
	    return $Hello_gets{$opt}.', sir';
    }
	return $Hello_gets{$opt};
}

sub Hello_Set($@) {
	my ($hash, @param) = @_;
	
	return '"set Hello" needs at least one argument' if (int(@param) < 2);
	
	my $name = shift @param;
	my $opt = shift @param;
	my $value = join("", @param);
	
	if(!defined($Hello_gets{$opt})) {
		my @cList = keys %Hello_gets;
		return "Unknown argument $opt, choose one of " . join(" ", @cList);
	}
    $hash->{STATE} = $Hello_gets{$opt} = $value;
    
	return "$opt set to $value. Try to get it.";
}


sub Hello_Attr(@) {
	my ($cmd,$name,$attr_name,$attr_value) = @_;
	if($cmd eq "set") {
        if($attr_name eq "formal") {
			if($attr_value !~ /^yes|no$/) {
			    my $err = "Invalid argument $attr_value to $attr_name. Must be yes or no.";
			    Log 3, "Hello: ".$err;
			    return $err;
			}
		} else {
		    return "Unknown attr $attr_name";
		}
	}
	return undef;
}

1;

=pod
=begin html

<a name="Hello"></a>
<h3>Hello</h3>
<ul>
    <i>Hello</i> implements the classical "Hello World" as a starting point for module development. 
    You may want to copy 98_Hello.pm to start implementing a module of your very own. See 
    <a href="http://www.fhemwiki.de/wiki/DevelopmentModuleIntro">DevelopmentModuleIntro</a> for an 
    in-depth instruction to your first module.
    <br><br>
    <a name="Hellodefine"></a>
    <b>Define</b>
    <ul>
        <code>define &lt;name&gt; Hello &lt;greet&gt;</code>
        <br><br>
        Example: <code>define HELLO Hello TurnUrRadioOn</code>
        <br><br>
        The "greet" parameter has no further meaning, it just demonstrates
        how to set a so called "Internal" value. See <a href="http://fhem.de/commandref.html#define">commandref#define</a> 
        for more info about the define command.
    </ul>
    <br>
    
    <a name="Helloset"></a>
    <b>Set</b><br>
    <ul>
        <code>set &lt;name&gt; &lt;option&gt; &lt;value&gt;</code>
        <br><br>
        You can <i>set</i> any value to any of the following options. They're just there to 
        <i>get</i> them. See <a href="http://fhem.de/commandref.html#set">commandref#set</a> 
        for more info about the set command.
        <br><br>
        Options:
        <ul>
              <li><i>satisfaction</i><br>
                  Defaults to "no"</li>
              <li><i>whatyouwant</i><br>
                  Defaults to "can't"</li>
              <li><i>whatyouneed</i><br>
                  Defaults to "try sometimes"</li>
        </ul>
    </ul>
    <br>

    <a name="Helloget"></a>
    <b>Get</b><br>
    <ul>
        <code>get &lt;name&gt; &lt;option&gt;</code>
        <br><br>
        You can <i>get</i> the value of any of the options described in 
        <a href="#Helloset">paragraph "Set" above</a>. See 
        <a href="http://fhem.de/commandref.html#get">commandref#get</a> for more info about 
        the get command.
    </ul>
    <br>
    
    <a name="Helloattr"></a>
    <b>Attributes</b>
    <ul>
        <code>attr &lt;name&gt; &lt;attribute&gt; &lt;value&gt;</code>
        <br><br>
        See <a href="http://fhem.de/commandref.html#attr">commandref#attr</a> for more info about 
        the attr command.
        <br><br>
        Attributes:
        <ul>
            <li><i>formal</i> no|yes<br>
                When you set formal to "yes", all output of <i>get</i> will be in a
                more formal language. Default is "no".
            </li>
        </ul>
    </ul>
</ul>

=end html

=cut

Der HTML-Code zwischen den Tags =pod und =cut dient zur Generierung der commandref.html. Der HTML-Inhalt wird automatisch beim Verteilen des Moduls im Rahmen des Update-Mechanismus aus jedem Modul extrahiert und daraus die Commandref in verschiedenen Sprachen erstellt. Eine detaillierte Beschreibung wie ein Commandref-Abschnitt in einem Modul definiert wird, siehe: Guidelines zur Dokumentation

Noch zu beschreiben

  • Zweistufiges Modell für Module
  • Funktion X_State_Fn: Thema, siehe auch DevelopmentState
  • FW_summaryFn (wird von FHEMWEB aufgerufen fuer Raum-Uebersicht)
  • FW_detailFn (wird von FHEMWEB aufgerufen fuer Detail-Ansicht)
  • DevIO
  • AsyncOutputFn / asyncOutput
  • SetExtensions / SetExtensionsCancel
  • ExceptFn (gleiche wie ReadFn aber EXCEPT_FD anstelle von FD)
  • FingerprintFn
  • ParseFn