Skip to content

Commit b7ec1ec

Browse files
sidey79claude
andcommitted
docs(adr): decide on sensor value decoding layer (ADR-006)
PySignalduino stops at the demodulated hex payload today, so only FHEM can turn a received frame into actual measurements. ADR-006 accepts option 4 of the FHEM integration proposal and lays out the second stage: * a dedicated signalduino/decoders/ layer between parser and output * SensorEvent plus one optional field on DecodedMessage, keeping every existing signature intact * data-driven JSON decoder specifications instead of one module per protocol, with a Python escape hatch for the irregular ones * three output adapters over one internal model: FHEM legacy string, rtl_433 JSON and Home Assistant discovery * topics that separate event from retained state, leaving the existing state/messages untouched Numbering note: 007 is already taken on feat/extendRAWmqtt, so this fills the open 006 slot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBqRw2DPXbSzhGBqB55cFZ
1 parent 23bbb26 commit b7ec1ec

2 files changed

Lines changed: 181 additions & 0 deletions

File tree

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
= ADR 006: Sensorwert-Dekodierschicht (Stufe 2)
2+
:doctype: article :encoding: utf-8 :lang: de :status: Accepted :decided-at: 2026-09-16 :decided-by: Architecture Owner
3+
4+
[NOTE]
5+
====
6+
Die Nummer 007 ist auf dem Branch `feat/extendRAWmqtt` bereits für das MQTT-Raw-Kommando vergeben
7+
und bleibt reserviert. Dieses ADR füllt bewusst die Lücke 006, damit die Nummerierung lückenlos
8+
bleibt.
9+
====
10+
11+
[#adr-context]
12+
== Kontext
13+
14+
PySignalduino deckt heute nur die erste Hälfte der Verarbeitungskette ab, die FHEM zweistufig löst:
15+
16+
. *Stufe 1 -- Demodulation:* Pulse und Pausen werden zu einer Bitfolge und daraus zu einem
17+
Hex-Payload. Das leistet PySignalduino vollständig über `signalduino/parser/` und `sd_protocols/`.
18+
. *Stufe 2 -- Interpretation:* Aus dem Hex-Payload werden konkrete Messwerte -- Temperatur,
19+
Luftfeuchte, Batteriezustand, Sensor-ID, Kanal. Diese Stufe fehlt vollständig; in FHEM liegt sie
20+
in den Client-Modulen wie `14_SD_WS.pm`.
21+
22+
Das Ergebnis der Verarbeitung ist heute `DecodedMessage(protocol_id, payload, raw, metadata)` aus
23+
`signalduino/types.py`, wobei `payload` ein Hex-String ist und `metadata` lediglich `bit_length`,
24+
`rssi` und `clock` enthält. Ein Feld für Messwerte existiert nicht.
25+
`sd_protocols/postdemodulation.py` prüft zwar Prüfsummen und Parität, liefert aber weiterhin nur
26+
bereinigte Bits -- das ist Validierung, keine Interpretation.
27+
28+
Daraus folgt die praktische Einschränkung, die dieses ADR auflöst: PySignalduino ist faktisch nur
29+
mit FHEM nutzbar, weil erst FHEM aus den Rohdaten Messwerte macht. Andere Konsumenten wie Home
30+
Assistant, Node-RED oder ioBroker müssten die Dekodierlogik jeweils selbst nachbauen.
31+
32+
Vorgearbeitet wurde bereits an drei Stellen:
33+
34+
* `sd_protocols/protocols.json` enthält für 160 Protokolle die aus FHEM übernommenen Felder
35+
`clientmodule`, `preamble`, `modulematch` und `postamble` -- die Datengrundlage für das Routing
36+
einer zweiten Stufe ist also vorhanden.
37+
* `sd_protocols/helpers.py` berechnet in `ConvLaCrosse()` bereits echte Messwerte, serialisiert sie
38+
aber sofort wieder in einen FHEM-Legacy-String, sodass sie verloren gehen.
39+
* `docs/architecture/proposals/fhem_mqtt_integration.adoc` beschreibt dieses Vorhaben als
40+
"Option 4" und bewertet es als Gold-Standard mit sehr hohem Aufwand, ohne es zu entscheiden.
41+
42+
[#adr-decision]
43+
== Entscheidung
44+
45+
Wir führen eine eigene Dekodierschicht `signalduino/decoders/` ein, die zwischen Parser und Ausgabe
46+
sitzt, sowie eine Adapterschicht `signalduino/output/`:
47+
48+
----
49+
transport -> parser (Stufe 1: Bits/Hex) -> decoders (Stufe 2: Werte) -> output (Adapter) -> mqtt
50+
----
51+
52+
*Ort der Schicht.* Die Dekodierung erfolgt weder in `sd_protocols/` noch im MQTT-Publisher.
53+
`sd_protocols/` ist die mechanische Portierung der Perl-Quellen und wird über `tools/convert.pl`
54+
regeneriert; dort abgelegter Code ginge beim nächsten Lauf verloren oder erzwänge Konfliktlösung.
55+
Der MQTT-Publisher scheidet aus, weil die Messwerte auch für `message_callback` und künftige
56+
Nicht-MQTT-Senken verfügbar sein müssen. Aufgerufen wird die Schicht in `signalduino/controller.py`
57+
innerhalb desselben `asyncio.to_thread`-Aufrufs wie der Parser, da es sich um CPU-gebundene
58+
Bitarithmetik handelt.
59+
60+
*Datenmodell.* Eine neue Dataclass `SensorEvent` nimmt Modell, Sensortyp, Geräte-ID, Kanal, die
61+
Messwerte und deren Einheiten auf. `DecodedMessage` erhält genau ein zusätzliches, optionales Feld
62+
`sensor`. Damit bleiben alle bestehenden Signaturen und der bestehende Nachrichtenfluss unverändert.
63+
Die Schlüssel in `SensorEvent.values` sind die FHEM-Readingnamen (`temperature`, `humidity`,
64+
`batteryState`, `channel`, ...). Diese Namen sind für rund 150 Protokolle ein De-facto-Standard;
65+
sie als kanonische Schlüssel zu verwenden macht den Paritätstest gegen die FHEM-Testdaten zu einem
66+
direkten Vergleich und die Mapping-Tabellen der Ausgabeadapter wiederverwendbar.
67+
68+
*Skalierung.* Die Dekodiervorschriften werden als validierte JSON-Spezifikationen abgelegt und von
69+
einem gemeinsamen Evaluator ausgewertet, statt pro Protokoll ein Python-Modul zu schreiben. Das
70+
Muster der FHEM-Client-Module ist überwiegend mechanisch -- Bitbereich, Skalierung, Offset,
71+
Vorzeichen, Wertetabelle -- und damit deklarativ abbildbar. JSON und nicht YAML, weil `jsonschema`
72+
bereits Abhängigkeit des Projekts ist. Für Protokolle, die sich nicht deklarativ beschreiben lassen,
73+
existiert eine Registrierung von Python-Dekodern als Ausweg.
74+
75+
*Routing.* Primärschlüssel ist die `protocol_id`, die in `DecodedMessage` bereits vorliegt. FHEM
76+
muss den Umweg über Präambel-Strings gehen, weil seine `Dispatch()`-Funktion nur einen String kennt;
77+
in Python wäre das ein unnötiger Informationsverlust. Die Felder aus `protocols.json` werden
78+
weiterhin genutzt: `preamble` zum Abtrennen des Hex-Teils und zum Aufbau des Legacy-Strings,
79+
`modulematch` als Vorabprüfung, `clientmodule` zur Gruppierung.
80+
81+
*Robustheit.* Stufe 2 darf Stufe 1 niemals beeinträchtigen. Der Dekodieraufruf ist vollständig
82+
gekapselt; schlägt er fehl, bleibt `sensor` leer und die bisherige Ausgabe unverändert. Für die
83+
überwiegende Mehrheit der Protokolle ist genau das zunächst der Normalfall.
84+
85+
*Ausgabeformate.* Drei Adapter rendern dasselbe interne Modell in unterschiedliche Zielformate:
86+
FHEM-Legacy-String, rtl_433-kompatibles JSON und Home-Assistant-MQTT-Discovery. Die Adapter sind
87+
reine Funktionen ohne MQTT-Kenntnis und einzeln über Umgebungsvariablen zuschaltbar.
88+
89+
*Topic-Struktur.* Die Topics trennen Ereignis von Zustand auf oberster Ebene:
90+
91+
[cols="2,3,1", options="header"]
92+
|===
93+
| Zweck | Topic | Retain
94+
95+
| Stufe 1 (unverändert)
96+
| `signalduino/v1/state/messages`
97+
| nein
98+
99+
| FHEM-Legacy (Ereignis)
100+
| `signalduino/v1/fhem/<clientmodule>/<protocol_id>`
101+
| nein
102+
103+
| rtl_433 (Ereignis)
104+
| `signalduino/v1/rtl433/<model>/<sensor_id>`
105+
| nein
106+
107+
| Kanonischer Zustand
108+
| `signalduino/v1/sensors/<device_id>`
109+
| ja
110+
111+
| Home-Assistant-Discovery
112+
| `homeassistant/<component>/pysd_<device_id>/<field>/config`
113+
| ja
114+
115+
| Verfügbarkeit (LWT)
116+
| `signalduino/v1/status`
117+
| ja
118+
|===
119+
120+
Ereignis-Topics tragen ein einzelnes Telegramm und werden nicht vorgehalten; Zustands-Topics tragen
121+
den letzten bekannten Wert eines Geräts und werden vorgehalten, damit ein neu verbundener Konsument
122+
sofort einen Wert sieht, statt auf das nächste Funktelegramm zu warten. Das bestehende
123+
`state/messages` bleibt bewusst unverändert, obwohl es nach dieser Einteilung ein Ereignis ist --
124+
eine Umbenennung wäre ein Bruch für bestehende Konsumenten und bleibt einer Version 2 des
125+
Topic-Schemas vorbehalten.
126+
127+
[#adr-consequences]
128+
== Konsequenzen
129+
130+
=== Positive Konsequenzen
131+
132+
* PySignalduino wird eigenständig nutzbar. Konsumenten erhalten unmittelbar verwertbare
133+
Schlüssel-Wert-Paare, ohne proprietäre Dekodierlogik im Zielsystem.
134+
* Home Assistant erkennt Sensoren über MQTT-Discovery automatisch; FHEM bleibt über den
135+
Legacy-Adapter vollständig bedienbar und verliert keine Funktion.
136+
* Ein neues Protokoll ist im Regelfall eine JSON-Datei plus ein Testvektor, kein Python-Code. Das
137+
ist die Voraussetzung dafür, perspektivisch Parität zu den FHEM-Client-Modulen zu erreichen.
138+
* Die FHEM-Testdaten unter `t/FHEM/<modul>/testData.json` enthalten neben den Rohtelegrammen auch
139+
die erwarteten Readings und dienen damit als unmittelbare Paritätsreferenz.
140+
* Der bestehende Nachrichtenfluss bleibt unangetastet; der FHEM-Adapter funktioniert ab dem ersten
141+
Tag für alle 160 Protokolle, weil er nicht von Stufe 2 abhängt.
142+
143+
=== Negative Konsequenzen
144+
145+
* Der Gesamtaufwand bis zur vollen Parität ist hoch; die Dekodierlogik von über 150 Protokollen
146+
wandert schrittweise in dieses Projekt und muss dort gepflegt werden.
147+
* Mit der Spezifikationssprache entsteht ein projekteigenes Format, das dokumentiert, versioniert
148+
und gegen ein Schema validiert werden muss.
149+
* Die Entscheidung, FHEM-Readingnamen als kanonische Schlüssel zu verwenden, bindet das Datenmodell
150+
an eine fremde Namenskonvention. Der Nutzen für Paritätstests und Migration überwiegt, die
151+
Abbildung auf andere Konventionen erfolgt in den Adaptern.
152+
* Solange für ein Protokoll keine Spezifikation existiert, liefert die Schicht keine Messwerte. Die
153+
Abdeckung wächst schrittweise und muss als Kennzahl sichtbar gemacht werden.
154+
* Zusätzliche Topics und vorgehaltene Discovery-Nachrichten erhöhen die Last auf dem Broker.
155+
156+
[#adr-alternatives]
157+
== Alternativen
158+
159+
* *Dekodierung in `sd_protocols/` (abgelehnt):* Naheliegend, weil dort bereits Protokollwissen
160+
liegt. Das Verzeichnis wird jedoch aus den Perl-Quellen generiert; neu geschriebener Code würde
161+
bei jeder Neugenerierung zum Konflikt.
162+
* *Dekodierung im MQTT-Publisher (abgelehnt):* Die Messwerte stünden dann ausschließlich dem
163+
MQTT-Pfad zur Verfügung und weder `message_callback` noch künftigen Senken.
164+
* *Ein Python-Modul pro Protokoll (abgelehnt):* Skaliert nicht auf über 150 Protokolle und führt zu
165+
massiver Codeverdopplung, da sich die Dekodierung überwiegend in Bitbereichen und Skalierungen
166+
erschöpft.
167+
* *Beibehaltung der FHEM-Bridge (Option 2 des Proposals, zurückgestellt):* Ein FHEM-Perl-Modul, das
168+
das JSON zurück in Dispatch-Strings übersetzt, wäre deutlich billiger, löst aber die
169+
Grundabhängigkeit von FHEM nicht auf und hilft anderen Zielsystemen nicht. Der Legacy-Adapter
170+
dieses ADR deckt den Kompatibilitätsbedarf ohne zusätzliches Perl-Modul ab.
171+
* *Neue Topic-Wurzel `events/` mit Migration von `state/messages` (zurückgestellt):* Semantisch
172+
sauberer, wäre aber ein Bruch für bestehende Konsumenten innerhalb von Version 1 des
173+
Topic-Schemas. Vorgesehen für Version 2.

‎docs/architecture/proposals/fhem_mqtt_integration.adoc‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@ PySignalDuino würde eine neue Konfigurationsoption erhalten, die es ihm erlaubt
6161

6262
=== Option 4: Portierung der Dekodier-Logik (Client-Module) nach PySignalDuino
6363

64+
[NOTE]
65+
====
66+
*Entschieden.* Diese Option wurde mit link:../decisions/ADR-006-sensor-decoding-layer.adoc[ADR-006]
67+
angenommen und dort ausgearbeitet. Der dort beschriebene FHEM-Legacy-Adapter deckt zusätzlich den
68+
Kompatibilitätsbedarf ab, den Option 2 adressiert hätte, ohne ein eigenes Perl-Modul zu erfordern.
69+
====
70+
6471
Anstatt nur Rohdaten zu senden, übernimmt PySignalDuino auch die Interpretation der Daten (z.B. Umrechnung von Hex-Werten in Temperatur, Luftfeuchtigkeit, Batteriestatus, Windgeschwindigkeit etc.). Das entspricht der Logik, die aktuell in FHEM-Modulen wie `14_SD_WS.pm` liegt.
6572

6673
[cols="1,3"]
@@ -88,6 +95,7 @@ PySignalDuino publiziert die demodulierten Nachrichten in einem standardisierten
8895
**Fazit:**
8996
* Varianten, die das standardisierte JSON von PySignalDuino beibehalten (Option 1, 1b, 2), sind für eine Koexistenz gut geeignet.
9097
* **Option 4** ist der klare Gewinner für moderne IoT-Landschaften (HA, Node-RED), erfordert aber den größten Aufwand in PySignalDuino.
98+
* **Umgesetzt wird Option 4**, entschieden in link:../decisions/ADR-006-sensor-decoding-layer.adoc[ADR-006]. Der Aufwand wird durch eine datengetriebene Dekodier-Spezifikation beherrschbar gemacht, sodass ein neues Protokoll im Regelfall eine JSON-Datei statt Python-Code ist.
9199

92100

93101
== Grobe POC-Implementierungen

0 commit comments

Comments
 (0)