DevelopmentGuidelines

Aus FHEMWiki
Wechseln zu: Navigation, Suche

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 für fhem 5.x zutrifft.

Readings

Ein Reading (Ablesewert) ist jeglicher Wert, Zustand, etc., der von einem Gerät ausgelesen werden kann. Beispiele dafür sind Messtemperatur, Luftfeuchtigkeit, Schaltzustand (an/aus), Firmwareversion, Empfangsstärke.

I/O-Devices und Clients

Um Readings durch fhem verarbeiten zu können, müssen die Daten erst über ein I/O-Device in den Rechner kommen. Beispiele: FHZ1300, CUL, CM11, M232.

Ein I/O-Device ist über einen Port (Beispiel für Unix: /dev/ttyS0) oder einen Socket ansprechbar.

Ein I/O-Device kann selbst ein Gerät sein, das Readings liefert (Beispiel: SCIVT, sowie die meisten anderen Schnittstellengeräte, wenn man die Geräteversion oder die Uptime mit zu den Readings zählt), oder die Readings von anderen Geräten (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 Gerät 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 Gerät kann sowohl aktive als auch passive Readings beinhalten. Bei Geräten mit aktiven Readings sind passive Readings meist Informationen wie Versionsnummer oder Uptime (CUL, CM11).

Geräte, die mehrere verschiedene Readings haben, senden diese häufig im Batch an fhem (Beispiel: FHT80b, KS300).

Mechanismus für aktive Readings

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

Mechanismus für passive Readings

Für 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? Geräte melden sich dann an der Polling-Infrastruktur an. Wenn ja, wie?
  • Wie wird waitIfInitNotDone korrekt eingesetzt?

Mechanismus für langsame Readings

Problemstellung: Längere Verarbeitungsprozesse in einzelnen Modulen (z.B. aufgrund von Netzwerklatenzen oder toten Geräten) halten fhem komplett an und verhindern die Verarbeitung von Events.

Lösung: Multiprocessing

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

Die Lieferung des Resultats an den Vaterprozess erfolgt durch eine Loopback Verbindung zu fhem via localhost.

Dieser Mechanismus wurde durch Rudolf König in ein entsprechendes Modul verpackt, so dass jedes andere FHEM-Modul diese Möglichkeiten direkt nutzen kann. Eine nähere Erklärung sowie eine Anleitung dazu findet man in dem Artikel → Blocking Call

Fragen:

  • Soll es diesen Mechanismus nur passive Readings geben oder gibt es Anwendungsfälle für aktive Readings?

Verworfene Alternative: Multithreading

Argumente gegen Multithreading:

  • Wird praktisch auf keiner kleinen Plattform unterstützt.
  • 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 Quellcodes, 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 muss (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) benötigt/verwendet/erkannt, z.B. NAME, NR, CHANGED
  • Device Readings: Rohdaten, die vom physischen Gerät ausgelesen werden (nicht interpretiert), z.B. rain_raw (Wippenschläge des Regensensors) in KS300
  • Hilfsvariablen: z.B. rain_raw_adj (rain_raw, jedoch bereinigt um die bei einigen KS300 auftretenden erratischen Sprünge) in KS300, Zwischenergebnisse von Daten/Werten, die zur Mittelwertberechnung benötigt werden
  • Logical Readings: ausgewertete Messdaten und Zustände, z.B. Temperatur, Niederschlag (nicht als Wippenschläge 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 Energiemessgeräten, 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 Gerät hinterlegt werden, müssen nicht mehr alle Notifies durchsucht werden, sondern es kann das Modul direkt ermitteln, ob ein Notify ausgelöst wird.

Ablage im Programm

Programmtechnisch werden die Attribute eines Moduls so in Behältern abgelegt, dass folgende Kriterien erfüllt sind:

  • Die Aufteilung ist möglichst einfach/minimalistisch.
  • Anhand des Behälters lässt sich entscheiden,
    • ob die Werte der darin enthaltenen Attribute über 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 Behälter:

readings

  • Logische Rubrik: Device Reading, Logical Reading, Derived Reading
  • Wird gespeichert
  • Alle vom Gerät gelieferten bzw. berechneten Werte, die den Endanwender interessieren. Keine Rohdaten vom Gerät, die erst interpretiert werden müssen (Wippenschläge 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 nicht gespeichert
  • Alle Werte, die für den Endanwender nicht direkt sinnvoll sind aber vom Modul für unterschiedliche Zwecke benötigt 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 Geräte-Werte, die nicht gespeichert werden müssen, weil entweder berechnet, oder aus der Definition hervorgehen.
  • Beispiele
$defs{emwz}{fhem}{def} = "1 75 900" 
$defs{emwz}{fhem}{lastIODev} = "CUL"

Verworfener Behälter STATE

  • Logische Rubrik: Device Reading, Logical Reading, Derived Reading
  • Wird gespeichert
  • Kurze(!) Zusammenfassung der wichtigsten Information des Geräts. Was man in einer Übersicht über die Geräte sehen möchte, z.B. eine Warnung oder die aktuelle Temperatur beim FHT80 oder der Zeitpunkt der letzten Aktivierung beim PIRI. Falls es das nicht gibt, dann einheitlich "defined". GGf. über Attribute am Modul steuerbar. Eigentlich ist es kein Behälter im üblichen Sinne, da es keine Untereinheiten hat.

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

Status

Kurze Zusammenfassung der wichtigsten Information des Geräts. Was man in einer Übersicht über die Geräte sehen möchte, 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 über das Attribut defaultReading am individuellen Gerät 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 für alle vom Modul nach aussen sichtbaren Bezeichner: a) die Bezeichnungen der Behälter für Readings, Fhem und Helper und der Untereinträge, 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 Geräteklassen 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 Unterbehälter:

  • value: Wert der Messgröße
  • time: Zeitpunkt, zu dem der Wert ermittelt wurde

Beispiel:

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

Readings enthalten grundsätzlich genau einen Wert und diesen ohne Einheit.

Einheitendarstellung

Die verwendete Einheit für 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 gemäß "FHEM-Standard", z.B. immer SI-Einheiten, Temperaturen in Celsius, etc.

BatteryReadings

Es gibt nur diese drei Readings für den Batteriestatus:

  • batteryState
  • batteryPercent
  • batteryVoltage

Wertebereich:

  • batteryState: ok|low
  • batteryPercent: \d{1,2}|100
  • batteryVoltage: \d+.\d+

Wichtig: das jeweilige Modul setzt nur die Readings, die es aus den aktuellen Daten vom Geraet bestimmen kann. Konkret: niemand kann sich darauf verlassen, welche der drei battery Readings vorhanden sind (es gibt nicht überall ein batteryState). Wenn das Gerät früher ein Percent gemeldet hat, aber in der letzten Nachricht nur state, dann wird das Percent Reading nicht angefasst. Aktuell wird noch diskutiert, was mit dem Reading für Laden ist. Mehr dazu hier: Beitrag im FHEM Forum

Zeitdarstellung in fhem

Zeiten werden im Programm grundsätzlich immer maschinenlesbar abgelegt, also sowohl in $defs{deviceName}{readings}{time} als auch z.B. bei der Speicherung des Zeitpunkts der Ausführung des nächsten at-Kommandos.

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

Argumente für maschinenlesbar:

  • vereinfacht Datumsarithmetik, z.B. die Berechnung der Zeitdauer zwischen zwei Ereignissen
  • kann vom Frontend in die regionale Darstellung des Anwenders übersetzt 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 für die Zeitbestimmung höher auflösende 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 Mac OS 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 für den Menschen bestimmt sind, B
  • In Logs, die für die Maschine bestimmt sind, also z.B. bei Weiterverarbeitung in einem GUI, A
  • In Listen (list, xmllist friendly), die für den Menschen bestimmt sind, B
  • In Listen (xmllist), die für 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 unabhängig davon, ob die optionale Zeitzonenangabe anhängt oder nicht (getestet mit Gnuplot v4.2 pl3). Für 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 Geräts aktualisiert werden, weil z.B. ein Datagramm empfangen oder zeitgesteuert Werte eingelesen wurden. Es wird durch drei Angaben

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

beschrieben:

  • das Gerät 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. Für das Gerät device namens deviceName wird ein Datagramm empfangen oder es werden zeitgesteuert Werte eingelesen.

1. Die Readings werden aktualisiert. Für X= 1..N:

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

2. Das Event wird erstellt und an DoTrigger übergeben.

3. DoTrigger informiert zunächst alle Clients mit inform-Wunsch.

4. Das Event wird sodann von DoTrigger nacheinander an alle NotifyFn in der alphabetischen Reihenfolge der Gerätenamen 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.

Zurückgestellte 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" ändern. Insofern könnte man theoretisch einen der beiden Verfahren (modify vs. attribute) ablösen.

1. Idee: define-Parameter dürfen nachträglich nicht geändert werden, auch wenn es sich um optionale Parameter handelt => nicht-modifizierbare Internals

Pros:

  • define-Parameter sind elementar für den Betrieb des Geräts und können nicht sinnvoll zur Programmlaufzeit geändert werden (z.B. Hauskode bei X10, FHTId bei FHT80b)
  • Aus den define-Parametern werden bei der Initialisierung des Geräts weitere Helper und Internals abgeleitet und gespeichert. Eine nachträgliche Änderung zieht Änderungen der Helper und Internals nach sich mit ggf. schwer durchschaubaren Nebeneffekten (z.B. corr1..corr4 bei EM, rainadjustment bei KS300). Auch die Logs ändern sich nicht nachträglich.

Contras:

  • Man will nicht ein Gerät löschen und neu anlegen, wenn es ausgetauscht wird.
  • modify sollte bleiben, weil es nützlich ist.

2. Idee: Attribute enthalten Werte, die ohne Nebeneffekte zur Laufzeit geändert werden können, weil sie z.B. ad-hoc ausgewertet werden.

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

3. Idee: Attribute beinhalten geräteunabhängige Meta-Informationen:

  • room
  • defaultReading

Contras:

  • Nach 2. und 3. müsste
attr my_at disabled

doch zu Definition gehoeren. Und "skip_next" auch.


Spezialitäten

  • beim FS20 bleibt alles, wie es ist, also model als Attribut, was Auswirkung auf die möglichen set Befehle hat.
  • 1wire setzt nach define das model Attribut, lässt aber eine Änderung nicht zu, oder wenn doch, dann muss sich dementsprechend verhalten, es macht ja evtl. Sinn es zu ändern. Was machbar ist, entscheidet der Modul-Author.

Damit ist "model" immer ein Attribut.

Fazit

Da es keine überzeugende Alternative zum Ist-Zustand in fhem 4.x gibt, wird b.a.w. nicht hierüber entschieden.

notify

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

In fhem 4.x bekommt jedes Gerät mit NotifyFn jedes Event mit. Alternativ könnte sich ein Gerät als EventListener mit einem Filter bei fhem anmelden. Dann könnten die Events nur an die NotifyFn verteilt werden, für die es ein Match auf den Filter gaebe.

Der Punkt wurde zurückgestellt, 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 für a) die Bezeichnungen der Behälter für 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 grundsätzlich maschinenlesbar
E6
Zulässige Zeitdarstellungen
  • Sekunden seit der Unix-Epoche
  • ISO8601 mit optionaler Zeitzonenangabe

ISO8601 mit _ statt T war nicht mehrheitsfähig.

E7
Verwendung der Zeitdarstellungen
E8
Readings enthalten grundsätzlich genau einen Wert und diesen ohne Einheit.
E9
Einheiten ergeben sich aus der Interfacespezifikation.
E10
Entscheidung für ASCII
E11
Die Readings werden standardisiert, indem Geräteklassen 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.