DevelopmentGuidelines

Aus FHEMWiki

Wichtiger Hinweis: Diese Seite enthält die Ideen der fhem-Entwickler, wie fhem in einer künftigen Version funktionieren soll. Es beschreibt nicht den Zustand der aktuellen Umsetzung in den Release- oder den SVN-Versionen.

Definitionen

TODO: Rudis kanonische Begriffe verwenden!

Allgemeines

Die Version von fhem, in der diese Guidelines umgesetzt werden sollen, wird mit fhem-NEU bezeichnet. Mit fhem 4.x wird die aktuelle Architektur bezeichnet, die auch fuer fhem 5.x zutrifft.

Readings

Ein Reading (Ablesewert) ist jeglicher Wert, Zustand, etc., der von einem Geraet ausgelesen werden kann. Beispiele dafuer sind Messtemperatur, Luftfeuchtigkeit, Schaltzustand (an/aus), Firmwareversion, Empfangsstaerke.

I/O-Devices und Clients

Um Readings durch fhem verarbeiten zu koennen, muessen die Daten erst ueber ein I/O-Device in den Rechner kommen. Beispiele: FHZ1300, CUL, CM11, M232.

Ein I/O-Device ist ueber einen Port (Beispiel fuer Unix: /dev/ttyS0) oder einen Socket ansprechbar.

Ein I/O-Device kann selbst ein Geraet sein, das Readings liefert (Beispiel: SCIVT, sowie die meisten anderen Schnittstellengeraete, wenn man die Geraeteversion oder die Uptime mit zu den Readings zaehlt), oder die Readings von anderen Geraeten (Clients) weiterleiten (Beispiel: FHZ1300).

Requests for Proposal

Aktualisierung der Readings

Aktive und passive Readings

Es sind zwei Arten von Readings zu unterscheiden:

  1. Readings, die vom Geraet aktiv mitgeteilt werden, entweder ad-hoc oder in regelmäßigen Zeitabständen (aktive Readings). Beispiele: measuredTemp bei FHT80B, wind bei KS300
  2. Readings, nach denen fhem die Geräte fragen muss (passive Readings). Beispiele: energyDay bei EM1010PC, counter bei M232, temperature bei OWTEMP

Dasselbe Geraet kann sowohl aktive als auch passive Readings beinhalten. Bei Geraeten mit aktiven Readings sind passive Readings meist Informationen wie Versionsnummer oder Uptime (CUL, CM11).

Geraete, die mehrere verschiedene Readings haben, senden diese haeufig im Batch an fhem (Beispiel: FHT80b, KS300).

Mechanismus fuer aktive Readings

TODO: Mechanismus mit ParseFn, GetFn, Match usw. beschreiben (wie in fhem 4.x)!

Mechanismus fuer passive Readings

Fuer passive Geräte gibt es eine Polling-Infrastruktur.

Status Quo in fhem 4.x:

In der Routine DEVICE_Define wird ein interner Timer gestartet, der die Updatefunktion aufruft. INTERVAL ist die Periode in Sekunden.

sub
DEVICE_Define($$) {
...
InternalTimer(gettimeofday()+$hash->{INTERVAL}, "DEVICE_GetUpdate", $hash, 0);
...
}

In der Routine DEVICE_GetUpdate werden dann die Readings vom Gerät geholt und der Timer wird mit dem gleichen Befehl erneut gestartet.

sub
DEVICE_GetUpdate($$) {
...
# start internal timer; do it at the beginning to achieve equal intervals no matter how long it takes to gather data
InternalTimer(gettimeofday()+$hash->{INTERVAL}, "DEVICE_GetUpdate", $hash, 1);
# gather data
...
}

Der letzte Paramater 0 oder 1 ist waitIfInitNotDone.

Fragen:

  • Kann/soll dieses Verfahren in einem Modul gekapselt werden? Geraete melden sich dann an der Polling-Infrastruktur an. Wenn ja, wie?
  • Wie wird waitIfInitNotDone korrekt eingesetzt?

Mechanismus fuer langsame Readings

Problemstellung: Laengere Verarbeitungsprozesse in einzelnen Modulen (z.B. aufgrund von Netzwerklatenzen oder toten Geraeten) halten fhem komplett an und verhindern die Verarbeitung von Events.

Loesung: Multiprocessing

Beim Multiprocessing wird der fhem-Prozess geforkt, wenn ein Reading vom Geraet eingelesen werden soll. Der Vaterprozess wird sofort fortgesetzt und erledigt weitere Aufgaben. Der Kindprozess holt die Readings vom Geraet, liefert sie an den Vaterprozess und verendet.

Die Lieferung an den Vaterprozess erfolgt durch eine Verbindung zu fhem via Netzwerk und Aufruf des Befehls

set myDevice childupdate reading1:value1 reading2:value2 ...

Beispielcode:

DEVICE_StartUpdate wird periodisch aufgerufen.

sub 
 DEVICE_StartUpdate($)
 {
      my ($hash) = @_;
      # %updates contains the names of the readings to be updated
      my %updates = (
            "reading1" => "",
            "reading2" => "",
            "reading3" => "",
      );
      # care for some prerequisites
      ...
      if($hash->{CHILDPID}) {
        # there is still the child process running => try again next time
        Log 2, "DEVICE: Child already forked: timeout too short?";
        return;
      }
      # fork a child process and return if we are the parent process
      return if(($hash->{CHILDPID} = fork));
      # if we are here, we are the child process
      my @ret;
      foreach my $r (sort keys %updates) {
        my $ret = ... # retrieve reading $r from device
        $ret = "" if(!defined($ret));
        last if($ret eq ""); # break on error
        push(@ret, $r . ":" . $ret); # append readingI:valueI to array
      }   
      # now connect to parent process and pass retrieved readings
      my @port = split(" ", $attr{global}{port});
      my $server = IO::Socket::INET->new(PeerAddr => "localhost:$port[0]");
      Log 0, "DEVICE: Can't connect to parent\n" if(!$server);
      syswrite($server, "set $hash->{NAME} childupdate ".join(" ",@ret)."\n");
      exit(0);
  } 


Aus der Routine DEVICE_Set wird DEVICE_ChildUpdate aufgerufen, wenn das Kommando "set childupdate ..." lautet:

sub
 DEVICE_ChildUpdate($$) {
   my ($hash, @ret)= @_;
   delete $hash->{CHILDPID};
   # evaluate readings
   ...
   # inform about changes
   ...
 }

Fragen:

  • Kann dieser Mechanismus in einem Modul gekapselt werden, damit nicht jedes Geraet den o.g. Kode replizieren muss?
  • Soll es diesen Mechanismus nur passive Readings geben oder gibt es Anwendungsfaelle fuer aktive Readings?

Verworfene Alternative: Multithreading

Argumente gegen Multithreading:

  • Wird praktisch auf keiner kleinen Plattform unterstuetzt.
  • Negative Erfahrungen (z.B. Probleme bei vielen 3rd-party-Komponenten)
  • Kaum Programmiererfahrung mit Multithreading in Perl.

Verabschiedete Richtlinien

Zeichensatz

Alle nach aussen sichtbaren Werte sind in der Zeichenkodierung ASCII (Codes 32..127). Dies vermeidet Konvertierungsprobleme bei der Bearbeitung des Quellkodes, bei der Ausgabe durch Perl, beim Transport in Internet-Protokollen und bei der Darstellung im Frontend/GUI.

Klassifizierung der Attribute

Die Attribute eines Moduls werden in logische Klassen (Rubriken) eingeteilt.

Sinn und Zweck

Die Rubriken helfen...

  • zu sortieren, welche Attribute bei (xml)list gezeigt werden und welche nicht,
  • zu definieren, welche Werte per save in die Sicherung kommen und welche nicht,
  • zu vereinbaren, welche Änderungen ein notify auslösen und welche nicht,
  • zu definieren, wie ein Attribut im $hash->{} abgelegt wird (in $hash->{}, $hash->{READINGS}, $hash->{INTERNALS}, ...),
  • zu gliedern, welche Namenskonventionen jeweils für Attribute verwendet werden (Kleinbuchstaben, Großbuchstaben, CamelCaps, ...),
  • abzugrenzen, wo der Entwickler sich an Vorgaben halten muß (z.B. bei System Internals) und wo er frei ist, Attribute zu erfinden oder wegzulassen, und
  • festzulegen, wo bzgl. der Inhalte Standards notwendig sind und wo nicht (z.B. bei "Logical Readings", wenn diese im GUI angezeigt werden sollten).

Logische Rubriken

Die folgenden Rubriken stellen eine sehr feine Einteilung dar:

  • System Internals: werden vom Framework (fhem.pl) benoetigt/verwendet/erkannt, z.B. NAME, NR, CHANGED
  • Device Readings: Rohdaten, die vom physischen Geraet ausgelesen werden (nicht interpretiert), z.B. rain_raw (Wippenschlaege des Regensensors) in KS300
  • Hilfsvariablen: z.B. rain_raw_adj (rain_raw, jedoch bereinigt um die bei einigen KS300 auftretenden erratischen Spruenge) in KS300, Zwischenergebnisse von Daten/Werten, welche zur Mittelwertberechnung benoetigt werden
  • Logical Readings: ausgewertete Messdaten und Zustaende, z.B. Temperatur, Niederschlag (nicht als Wippenschlaege sondern in l/qm)
  • Derived Readings: aus den Messdaten abgeleitete Werte, z.B. durchschnittliche Temperatur der laufenden Woche, Niederschlagsmenge des Tages
  • Physical Device Parameter: z.B. Modell oder Housecode und Unitcode bei X10 oder die Sensornummer beim BS (brightness sensor)
  • Logical Device Parameter: statische Werte, die als Grundlage für Berechnungen genutzt werden, z.B. Korrekturfaktoren bei den Energiemessgeraeten, die Tankgeometrie beim USF1000
  • Messages: alle Nachrichten, die das Gerät betreffen, z.B. "AVG_Month erfolgreich berechnet", "Script XYZ am xx.xx.xx gestartet", LastIODEV, LastRAWMSG
  • Trigger: wenn Trigger direkt am Geraet hinterlegt werden, muessen nicht mehr alle Notifies durchsucht werden, sondern es kann das Modul direkt ermitteln, ob ein Notify ausgeloest wird.

Ablage im Programm

Programmtechnisch werden die Attribute eines Moduls so in Behaeltern abgelegt, dass folgende Kriterien erfuellt sind:

  • Die Aufteilung ist moeglichst einfach/minimalistisch.
  • Anhand des Behaelters laesst sich entscheiden,
    • ob die Werte der darin enthaltene Attribute ueber das Programmende hinaus gespeichert oder nicht gespeichert werden,
    • welchen Anzeigestandards oder Programmierstandards die darin enthaltenen Attribute entsprechen, und
    • ob es sich um fhem-interne Attribute, Readings oder Hilfsvariablen handelt.

Es gibt folgende Behaelter:

readings

  • Logische Rubrik: Device Reading, Logical Reading, Derived Reading
  • Wird gespeichert
  • Alle vom Geraet gelieferten bzw. berechneten Werte, die den Endanwender interessieren. Keine Rohdaten vom Geraet, die erst interpretiert werden muessen (Wippenschlaege beim Regensensor).
  • Alle Daten haben einen Wert und ein Zeitstempel
  • Beispiele:
$defs{Lampe}{readings}{switchedTo}{value} = "on"; $defs{Lampe}{readings}{switchedTo}{time} = "2010-03-29 23:32:26"
$defs{FHToben}{readings}{measuredTemp}{value} = "23"; $defs{FHToben}{readings}{switchedTo}{time} = "2010-03-29 21:58:36"

helper

  • Logische Rubrik: Hilfsvariable, Device Reading
  • Wird gespeichert
  • Alle Werte, die fuer den Endanwender nicht direkt Sinn machen, aber vom Modul fuer unterschiedliche Zwecke benoetigt werden. Kann auch rohe Readings enthalten.
  • Beispiele:
$defs{ks300}{helper}{cumMonth} = "23 T: 131.4  H: 816  W: 775.1  R: -651.1"
$defs{emwz}{helper}{basis} = "4776493"

fhem

  • Logische Rubrik: System Internals, Physical Device Parameter, Logical Device Parameter
  • Wird nicht gespeichert
  • Alle Geraete-Werte, die nicht gespeichert werden muessen, weil entweder berechnet, oder aus der Definition hervorgehen.
  • Beispiele
$defs{emwz}{fhem}{def} = "1 75 900" 
$defs{emwz}{fhem}{lastIODev} = "CUL"

Verworfener Behaelter STATE

  • Logische Rubrik: Device Reading, Logical Reading, Derived Reading
  • Wird gespeichert
  • Kurze(!) Zusammenfassung der wichtigsten Information des Geraetes. Was man in einer Uebersicht ueber die Geraete sehen moechte, z.B. eine Warnung oder die aktuelle Temperatur beim FHT80 oder der Zeitpunkt der letzten Aktivierung beim PIRI. Falls es sowas nicht gibt, dann einheitlich "defined". GGf. ueber Attribute am Modul steuerbar. Eigentlich ist es kein Behaelter im ueblichen Sinne, da es keine Untereinheiten hat.

Der Behaelter wurde verworfen, weil er redundant ist, an sich gar kein Behaelter, und einfacher und flexibler ueber einen Verweis auf das Reading realisiert werden kann, der per Default angezeigt werden soll.

Status

Kurze Zusammenfassung der wichtigsten Information des Geraetes. Was man in einer Uebersicht ueber die Geraete sehen moechte, z.B. eine Warnung oder die aktuelle Temperatur beim FHT80 oder der Zeitpunkt der letzten Aktivierung beim PIRI. Falls es sowas nicht gibt, dann einheitlich "defined". Modulseitig gibt es einen Default, der auf das relevante Reading verweist. Dieser ist ueber das Attribut defaultReading am individuellen Geraet steuerbar.

Beispiel: Definition in der Konfigurationsdatei: attr myDevice defaultReading measuredTemp Verwendung im Code:

$defs{myDevice}{readings}{$attr{defaultReading}}

Die logischen Rubriken Messages und Trigger wurden noch nicht diskutiert.

Bezeichnungen

Verwendung von lowerCamelCaps fuer alle vom Modul nach aussen sichtbaren Bezeichner: a) die Bezeichnungen der Behaelter fuer Readings, Fhem und Helper und der Untereintraege, b) die Bezeichnungen der Readings, c) die Bezeichnungen der Attribute.

Beispiel:

$defs{myFHT}{readings}{measuredTemp}{time}

Readings

Standardisierung der Readings

Die Readings werden standardisiert, indem Geraeteklassen gebildet werden wie in DevelopmentInterfaces beschrieben. Die Definition der Interfaces ist explizit nicht Gegenstand dieser Entscheidungsvorlage und wird weiterentwickelt und angepasst, wie es sich bei der Entwicklung von fhem-NEU ergibt. Struktur im Code

Zu jedem Reading gibt es die folgenden Unterbehaelter:

  • value: Wert der Messgroesse
  • time: Zeitpunkt, zu dem der Wert ermittelt wurde

Beispiel:

$defs{myFHT}{readings}{measuredTemp}{time}= timestamp;
$defs{myFHT}{readings}{measuredTemp}{value}= 23.5;

Readings enthalten grundsaetzlich genau einen Wert und diesen ohne Einheit.

Einheitendarstellung

Die verwendete Einheit fuer ein Reading ergibt sich aus der Interfacespezifikation.

Beispiel: Gerät ist ein Thermometer => es gibt ein Reading "temperature" und dessen Einheit ist immer °C oder Celsius

Verworfene Alternative:

  • Einheit ergibt sich aus der Dokumentation
  • Einheit ergibt sich aus einem expliziten Untereintrag $defs{deviceName}{readings}{theReading}{unit}="°C"
  • Einheit ergibt sich aus Festlegung gemaess "FHEM-Standard", z.B. immer SI-Einheiten, Temperaturen in Celsius, etc.

Zeitdarstellung in fhem

Zeiten werden im Programm grundsaetzlich immer maschinenlesbar abgelegt, also sowohl in $defs{deviceName}{readings}{time} als auch z.B. bei der Speicherung des Zeitpunkts der Ausfuehrung des naechsten at-Kommandos.

Es gibt uebergreifende Hilfsfunktionen, die die maschinenlesbare in eine menschenlesbare Darstellung umwandeln.

Argumente fuer maschinenlesbar:

  • vereinfacht Datumsarithmetik, z.B. die Berechnung der Zeitdauer zwischen zwei Ereignissen
  • kann vom Frontend in die regionale Darstellung des Anwenders uebersetzt werden
  • hat keine Probleme mit doppelt auftretenden Zeitpunkten bei der Umstellung von Sommer- auf Winterzeit

Folgende Zeitdarstellungen werden ausschliesslich verwendet:

A. Zahl der Sekunden seit der Unix-Epoche (was time liefert); auch wenn time eine Ganzzahl liefert, muss der Verwender damit rechnen, eine Gleitkommazahl vorzufinden. Das ist z.B. dann der Fall, wenn fuer die Zeibestimmung hoeheraufloesende Funktionen (z.B. Time::HiRes) zum Einsatz kamen und Sekundenbruchteile mitgespeichert wurden.

Hinweis zu potentiellen Problemen:

  • Ab perl 5.12 ist das Jahr-2038-Problem in Perl beseitigt.
  • Ab MacOS X liefert time auch auf Macs die Zahl der Sekunden seit der Unix-Epoche (statt seit Anfang 1904)

B. ISO8601 mit optionaler Zeitzonenangabe, wobei bei fehlender Zeitzone die lokale Zeitzone des fhem-Servers gilt

Die Zeitdarstellungen werden wie folgt verwendet:

  • Zur Programmlaufzeit in Variablen immer A
  • In Konfigurationsdateien (fhem.conf, fhem.save) immer B
  • In Logs, die fuer den Menschen bestimmt sind, B
  • In Logs, die fuer die Maschine bestimmt sind, also z.B. bei Weiterverarbeitung in einem GUI, A
  • In Listen (list, xmllist friendly), die fuer den Menschen bestimmt sind, B
  • In Listen (xmllist), die fuer die Maschine bestimmt sind, also z.B. bei Weiterverarbeitung in einem GUI, A

Die Unterscheidung bei den Logs ist noch zu diskutieren.

Hinweise zu gnuplot:

set timefmt '%Y-%m-%dT%H:%M:%S' 

(ISO8601) funktioniert, und zwar unabhaengig davon, ob die optionale Zeitzonenangabe anhaengt oder nicht (getestet mit Gnuplot v4.2 pl3). Fuer die Sekunden seit der Unix-Epoche:

set timefmt '%s' 

(nicht getestet).

Allgemeine Dokumentation

Events, Filter, Notify

Event

Ein Event tritt ein, wenn ein oder mehrere Readings eines Geraetes aktualisiert werden, weil z.B. ein Datagramm empfangen oder zeitgesteuert Werte eingelesen wurden. Es wird durch drei Angaben

(device, timestamp, { reading1, ..., readingN } )

beschrieben:

  • das Geraet device
  • der Zeitpunkt der Aktualisierung timestamp
  • die Menge der N>= 1 geaenderten Readings reading1, .. readingN

Wenn N> 1, dann handelt es sich um ein Sammelevent, wie es z.B. von KS300 generiert wird.

Filter

Ein Filter filtert Events. Er hat die Form

deviceNamePattern

bzw.

deviceNamePattern:readingNamePattern 

Ein Filter passt auf ein Event, wenn deviceNamePattern den Namen des device matcht und, sofern vorhanden, readingNamePattern mindestens irgendeinen Namen von reading1 bis readingN matcht.

Mechanismus

0. Fuer das Geraet device namens deviceName wird ein Datagramm empfangen oder es werden zeitgesteuert Werte eingelesen.

1. Die Readings werden aktualisiert. Fuer X= 1..N:

$defs{deviceName}{readings}{readingX}{value}= valueX;
$defs{deviceName}{readings}{readingX}{time}= timestamp;

2. Das Event wird erstellt und an DoTrigger uebergeben.

3. DoTrigger informiert zunaechst alle Clients mit inform-Wunsch.

4. Das Event wird sodann von DoTrigger nacheinander an alle NotifyFn in der alphabetischen Reihenfolge der Geraetenamen gereicht. Darunter auch jene von Logs.

5. Wenn der Filter des Logs auf das Event passt, wird ein Log-Eintrag der Form

timestamp deviceName reading1: value1 reading2: value2 ... readingN: valueN

erzeugt.

Zurueckgestellte Entscheidungen

Attribute vs. Internals

Die Unterscheidung zwischen Attributen und Internals ist nicht eindeutig. Manche define-Parameter sind optional, und man kann define-Parameter mit "modify" aendern. Insofern koennte man theoretisch einen der beiden Verfahren (modify vs. attribute) abloesen.

1. Idee: define-Parameter duerfen nachtraeglich nicht geaendert werden, auch wenn es sich um optionale Parameter handelt => nicht-modifizierbare Internals

Pros:

  • define-Parameter sind elementar fuer den Betrieb des Geraetes und koennen nicht sinnvoll zur Programmlaufzeit geaendert werden (z.B. Hauskode bei X10, FHTId bei FHT80b)
  • Aus den define-Parametern werden bei der Initialisierung des Geraets weitere Helper und Internals abgeleitet und gespeichert. Eine nachtraegliche Aenderung zieht Aenderungen der Helper und Internals nach sich mit ggf. schwer durchschaubaren Nebeneffekten (z.B. corr1..corr4 bei EM, rainadjustment bei KS300). Auch die Logs aendern sich nicht nachtraeglich.

Contras:

  • Man will nicht ein Geraet loeschen und neu anlegen, wenn es ausgetauscht wird.
  • modify sollte bleiben, weil es nuetzlich ist.

2. Idee: Attribute enthalten Werte, die ohne Nebeneffekte zur Laufzeit geaendert werden koennen, weil sie z.B. ad-hoc ausgewertet werden.

  • follow-on-for-timer
  • retrycount
  • lazy

3. Idee: Attribute beinhalten geraeteunabhaengige Meta-Informationen:

  • room
  • defaultReading

Contras:

  • Nach 2. und 3. muesste
attr my_at disabled

doch zu Definition gehoeren. Und "skip_next" auch.


Spezialitaeten

  • beim FS20 bleibt alles, wie es ist, also model als Attribut, was Auswirkung auf die moeglichen set Befehle hat.
  • 1wire setzt nach define das model Attribut, laesst aber eine Aenderung nicht zu, oder wenn doch, dann muss sich dementsprechend verhalten, es macht ja evtl. Sinn es zu aendern. Was machbar ist, entscheidet der Modul-Author.

Damit ist "model" immer ein Attribut.

Fazit

Da es keine ueberzeugende Alternative zum Ist-Zustand in fhem 4.x gibt, wird b.a.w. nicht hierueber entschieden.

notify

Die Frage, ob der notify-Mechanismus, der weiter oben dokumentiert ist, aus Performancegruenden optimiert werden sollte, ist offen.

In fhem 4.x bekommt jedes Geraet mit NotifyFn jedes Event mit. Alternativ koennte sich ein Geraet als EventListener mit einem Filter bei fhem anmelden. Dann koennten die Events nur an die NotifyFn verteilt werden, fuer die es ein Match auf den Filter gaebe.

Der Punkt wurde zurueckgestellt, bis nachgewiesen ist, dass sich aus dieser Vorgehensweise relevante Auswirkungen auf die Systemlast ergeben.

Entscheidungen

E1
Es werden die Container fhem, readings und helper verwendet.
E2
Es wird ein Attribut defaultReading verwendet.
E3
Verwendung von lowerCamelCaps fuer a) die Bezeichnungen der Behaelter fuer Readings, Fhem und Helper und der Untereintraege, b) die Bezeichnungen der Readings, c) die Bezeichnungen der Attribute.
E4
Verwendung von value und time.
E5
Zeitdarstellung im Programm grundsaetzlich maschinenlesbar
E6
Zulaessige Zeitdarstellungen
  • Sekunden seit der Unix-Epoche
  • ISO8601 mit optionaler Zeitzonenangabe

ISO8601 mit _ statt T war nicht mehrheitsfaehig.

E7
Verwendung der Zeitdarstellungen
E8
Readings enthalten grundsaetzlich genau einen Wert und diesen ohne Einheit.
E9
Einheiten ergeben sich aus der Interfacespezifikation.
E10
Entscheidung fuer ASCII
E11
Die Readings werden standardisiert, indem Geraeteklassen gebildet werden wie in DevelopmentInterfaces beschrieben. Die Definition der Interfaces ist explizit nicht Gegenstand dieser Entscheidungsvorlage und wird weiterentwickelt und angepasst, wie es sich bei der Entwicklung von fhem-NEU ergibt.