|
| 1 | += Decoder-Spezifikationen (Stufe 2) |
| 2 | +:sectlinks: |
| 3 | + |
| 4 | +Der Empfang läuft in zwei Stufen, wie in link:../architecture/decisions/ADR-006-sensor-decoding-layer.adoc[ADR-006] beschrieben: |
| 5 | + |
| 6 | +. *Stufe 1* — `signalduino/parser` und `sd_protocols` machen aus Pulsen eine Bitfolge und daraus einen Hex-Payload. |
| 7 | +. *Stufe 2* — `signalduino/decoders` macht daraus Messwerte: Temperatur, Luftfeuchte, Batteriezustand, Sensor-ID, Kanal. |
| 8 | + |
| 9 | +Dieses Kapitel beschreibt, wie man Stufe 2 um ein Protokoll erweitert. |
| 10 | + |
| 11 | +== Der Regelfall: eine JSON-Datei |
| 12 | + |
| 13 | +Die meisten Protokolle lassen sich vollständig als Daten beschreiben. Eine Spezifikation liegt unter `signalduino/decoders/specs/` und wird beim Start gegen `spec_schema.json` validiert; ein Tippfehler fällt damit sofort beim Laden auf und nicht still bei jedem Telegramm. |
| 14 | + |
| 15 | +[source,json] |
| 16 | +---- |
| 17 | +{ |
| 18 | + "protocol_id": "900", |
| 19 | + "model": "TEST_900", |
| 20 | + "sensor_type": "Beispielgerät", |
| 21 | + "prematch": "^12", |
| 22 | + "crc": { |
| 23 | + "algorithm": "crc8", |
| 24 | + "data": { "from": 0, "to": 5 }, |
| 25 | + "check": { "from": 6, "to": 7 } |
| 26 | + }, |
| 27 | + "fields": { |
| 28 | + "id": { "source": "hex", "from": 0, "to": 1, "type": "str" }, |
| 29 | + "channel": { "source": "bits", "from": 17, "to": 19, "offset": 1 }, |
| 30 | + "temperature": { "source": "bits", "from": 22, "to": 31, |
| 31 | + "type": "float", "offset": -400, "scale": 0.1, "unit": "°C" } |
| 32 | + }, |
| 33 | + "limits": { "temperature": [-40, 60] } |
| 34 | +} |
| 35 | +---- |
| 36 | + |
| 37 | +Die Namen unter `fields` sind bewusst die FHEM-Readingnamen (`temperature`, `humidity`, `batteryState`, `channel`, ...). Sie sind über rund 150 Protokolle hinweg etabliert, machen den Paritätstest gegen die FHEM-Testvektoren zu einem direkten Vergleich und werden von den Ausgabeadaptern auf deren jeweilige Namen abgebildet. |
| 38 | + |
| 39 | +== Indizes |
| 40 | + |
| 41 | +`from` und `to` sind **nullbasiert und einschließlich** — `from: 18, to: 27` ist ein Zehn-Bit-Feld. Das entspricht `SD_WS_binaryToNumber($bitData, 18, 27)` in FHEM, sodass sich die Bitpositionen aus den Kommentaren der Perl-Module direkt übernehmen lassen. |
| 42 | + |
| 43 | +`source` wählt die Datenquelle: |
| 44 | + |
| 45 | +* `bits` — die demodulierte Bitfolge aus `metadata["bits"]`. Fehlt sie, etwa bei MN-Telegrammen, wird sie aus dem Hex-Payload erzeugt. |
| 46 | +* `hex` — die Zeichen des Hex-Payloads, ohne Präambel. |
| 47 | + |
| 48 | +== Rechenschritte eines Feldes |
| 49 | + |
| 50 | +Die Reihenfolge liegt fest und ist so gewählt, dass eine Regel sich liest wie die FHEM-Zeile, die sie ersetzt: |
| 51 | + |
| 52 | +. Bereich ausschneiden |
| 53 | +. `bcd` — als binär codierte Dezimalzahl lesen |
| 54 | +. `sign_bit` mit `sign_style` anwenden |
| 55 | +. `offset` addieren |
| 56 | +. `scale` multiplizieren |
| 57 | +. `round` auf Nachkommastellen kürzen |
| 58 | +. `map` — Rohwert über eine Wertetabelle abbilden |
| 59 | +. `derive` — abgeleiteten Wert berechnen |
| 60 | + |
| 61 | +[cols="1,3", options="header"] |
| 62 | +|=== |
| 63 | +| Schlüssel | Bedeutung |
| 64 | + |
| 65 | +| `type` |
| 66 | +| `int` (Vorgabe), `float`, `str`, `bool`. `str` liefert den Rohausschnitt, solange die Regel keine Rechenoperation enthält — typisch für Sensor-IDs. |
| 67 | + |
| 68 | +| `sign_style` |
| 69 | +| `offset` zieht `sign_offset` ab (das FHEM-Idiom `wert - 1024`), `negate` kehrt das Vorzeichen um, `twos_complement` liest den Bereich im Zweierkomplement. |
| 70 | + |
| 71 | +| `sign_value` |
| 72 | +| Welcher Wert des Vorzeichenbits negativ bedeutet, Vorgabe `"1"`. |
| 73 | + |
| 74 | +| `map` |
| 75 | +| Bildet den Rohwert ab, z. B. `{"0": "ok", "1": "low"}` für den Batteriezustand. |
| 76 | + |
| 77 | +| `derive` |
| 78 | +| Abgeleiteter Wert, derzeit `wind_dir_text`. Mit `derive_from` rechnet die Regel auf einem bereits dekodierten Feld statt auf einem eigenen Bereich. |
| 79 | + |
| 80 | +| `unit` |
| 81 | +| Einheit, die im `SensorEvent` neben dem Wert geführt wird. |
| 82 | +|=== |
| 83 | + |
| 84 | +== Prüfsummen |
| 85 | + |
| 86 | +`crc` verweist auf einen Algorithmus aus `signalduino/decoders/crc.py` und auf zwei Bereiche des Hex-Payloads: `data` ist der geprüfte Bereich, `check` die im Telegramm mitgesendete Prüfsumme. Beide Bereiche sind Zeichenindizes und müssen ganze Bytes umfassen. |
| 87 | + |
| 88 | +Verfügbar sind `crc8`, `crc16`, `crc16lsb`, `sum8`, `xor8`, `lfsr_digest8` und `lfsr_digest8_reflect`. Abweichende Parameter stehen unter `params`, etwa `{"poly": 49, "init": 255}`. |
| 89 | + |
| 90 | +Schlägt die Prüfung fehl, wird das Telegramm verworfen — es entsteht kein `SensorEvent`. Genauso wirkt eine Verletzung von `limits`; das entspricht dem Verhalten der FHEM-Module, die bei unplausiblen Werten ebenfalls nichts liefern. |
| 91 | + |
| 92 | +== Varianten |
| 93 | + |
| 94 | +Trägt ein Protokoll je nach Telegrammtyp unterschiedliche Felder, wählt `variant_selector` den passenden Block aus `variants` aus. Die Felder unter `fields` gelten für alle Varianten, der Variantenblock ergänzt und überschreibt sie; `model` und `sensor_type` lassen sich pro Variante anpassen. |
| 95 | + |
| 96 | +[source,json] |
| 97 | +---- |
| 98 | +{ |
| 99 | + "variant_selector": { "source": "hex", "from": 0, "to": 1, "type": "str" }, |
| 100 | + "fields": { "id": { "source": "hex", "from": 2, "to": 3, "type": "str" } }, |
| 101 | + "variants": { |
| 102 | + "30": { "model": "SD_WS_125_TH", "fields": { "temperature": { "…": "…" } } }, |
| 103 | + "52": { "model": "SD_WS_125_T", "fields": { "…": "…" } } |
| 104 | + } |
| 105 | +} |
| 106 | +---- |
| 107 | + |
| 108 | +== Der Ausnahmefall: ein Python-Decoder |
| 109 | + |
| 110 | +Lässt sich ein Protokoll nicht als Daten beschreiben — etwa weil Felder voneinander abhängen, die Prüfsumme über einen umsortierten Payload läuft oder der Modellname selbst aus den Daten entsteht — gehört es nach `signalduino/decoders/custom/`: |
| 111 | + |
| 112 | +[source,python] |
| 113 | +---- |
| 114 | +from ..registry import register_decoder |
| 115 | +
|
| 116 | +@register_decoder("115") |
| 117 | +def decode_bresser_5in1(message): |
| 118 | + ... |
| 119 | + return SensorEvent(...) |
| 120 | +---- |
| 121 | + |
| 122 | +Ein solcher Decoder hat Vorrang vor einer gleichnamigen Spezifikation. Damit lässt sich ein Protokoll später von der einen Form in die andere überführen, ohne etwas zu löschen. |
| 123 | + |
| 124 | +== Routing |
| 125 | + |
| 126 | +Gefunden wird ein Decoder über die `protocol_id`, die in `DecodedMessage` bereits vorliegt. Die Felder aus `protocols.json` dienen dabei nur der Vorbereitung: `preamble` trennt den Hex-Teil ab, `modulematch` ist der Vorgabewert für `prematch`, und `clientmodule` gruppiert die Protokolle für Topics und Berichte. |
| 127 | + |
| 128 | +== Neues Protokoll hinzufügen |
| 129 | + |
| 130 | +. Bitlayout und Prüfsumme im zugehörigen FHEM-Client-Modul nachlesen, etwa in `14_SD_WS.pm`. |
| 131 | +. Spezifikation unter `signalduino/decoders/specs/` anlegen. |
| 132 | +. Testvektoren vorhanden? `tests/data/fhem/` enthält die FHEM-Vektoren samt erwarteter Readings; ergänzt werden sie mit `tools/fhem_testdata_import.py`. |
| 133 | +. Tests ausführen (siehe `AGENTS.md`, Abschnitt zur Testausführung im Devcontainer). |
| 134 | + |
| 135 | +Stufe 2 kann Stufe 1 nicht beschädigen: Jeder Fehler im Decoder führt zu `sensor = None`, während der bisherige Nachrichtenfluss unverändert weiterläuft. |
0 commit comments