diff --git a/.devcontainer/fhem-data/FHEM/99_MyUtils.pm b/.devcontainer/fhem-data/FHEM/99_MyUtils.pm new file mode 100644 index 0000000..7f1cf32 --- /dev/null +++ b/.devcontainer/fhem-data/FHEM/99_MyUtils.pm @@ -0,0 +1,18 @@ +############################################## +# $Id: 99_MyUtils.pm 1932 2012-01-28 18:15:28Z martinp876 $ +package main; + +use strict; +use warnings; +use JSON; +use Data::Dumper; + +sub MyUtils_Initialize { + my ($hash) = @_; +} + +# Enter you functions below _this_ line. + +# MqttSignalduino_DispatchFromJSON wurde nach FHEM/lib/SD_Dispatch.pm verschoben. + +1; diff --git a/.devcontainer/fhem-data/fhem_signalduino_example.cfg b/.devcontainer/fhem-data/fhem_signalduino_example.cfg index 5e93c36..5822557 100755 --- a/.devcontainer/fhem-data/fhem_signalduino_example.cfg +++ b/.devcontainer/fhem-data/fhem_signalduino_example.cfg @@ -11,6 +11,7 @@ attr global verbose 3 # # This file is loaded by the FHEM container via CONFIGTYPE environment variable. +define autocreate autocreate # 1. Define FHEMWEB instance to access FHEM via Browser (Port 8083) define WEB FHEMWEB 8083 global @@ -39,9 +40,10 @@ attr mqtt_broker autocreate simple define PySignalDuino MQTT2_DEVICE setuuid PySignalDuino 695e9c21-f33f-c986-4f81-a9f0ab37b6bcedf8 attr PySignalDuino IODev mqtt_broker -attr PySignalDuino readingList signalduino/v1/state/messages:.* { json2nameValue($EVENT, 'MSG_',$JSONMAP) }\ +attr PySignalDuino readingList signalduino/v1/state/messages:.* { use FHEM::Devices::SIGNALDuino::Message;; FHEM::Devices::SIGNALDuino::Message::json2Dispatch($EVENT, $NAME);; json2nameValue($EVENT,'MSG_');; }\ signalduino/v1/responses:.* { json2nameValue($EVENT, 'RESP_') }\ signalduino/v1/errors:.* { json2nameValue($EVENT, 'ERR_') } +# attr PySignalDuino Clients :CUL_EM:CUL_FHTTK:CUL_TCM97001:CUL_TX:CUL_WS:Dooya:FHT:FLAMINGO:FS10:FS20:Fernotron:Hideki:IT:KOPP_FC:LaCrosse:OREGON:PCA301:RFXX10REC:Revolt:SD_AS:SD_Rojaflex:SD_BELL:SD_GT:SD_Keeloq:SD_RSL:SD_UT:SD_WS07:SD_WS09:SD_WS:SD_WS_Maverick:SOMFY:Siro:SIGNALduino_un: attr PySignalDuino setList raw:textField signalduino/v1/commands/set/raw $EVTPART1 \ cc1101_reg:textField signalduino/v1/commands/set/cc1101_reg $EVTPART1 \ # System GET commands (noArg) \ diff --git a/.gitignore b/.gitignore index 7e572d4..18ddff3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,13 +2,15 @@ pycache/ *.pyc .venv/ .env/ -temp_repo/ SIGNALDuino-Firmware/ .devcontainer/devcontainer.env .devcontainer/.devcontainer.env .devcontainer/mosquitto/data/ .devcontainer/mosquitto/log/ .devcontainer/fhem-data/* +!.devcontainer/fhem-data/FHEM +.devcontainer/fhem-data/FHEM/* !.devcontainer/fhem-data/fhem_signalduino_example.cfg +!.devcontainer/fhem-data/FHEM/99_MyUtils.pm .roo/mcp.json diff --git a/README.adoc b/README.adoc index ce1f942..4c79e6c 100644 --- a/README.adoc +++ b/README.adoc @@ -39,6 +39,14 @@ Die SIGNALDuino-Firmware (Microcontroller-Code) wird in einem separaten Reposito * **Ausführbares Hauptprogramm** – `main.py` bietet eine sofort einsatzbereite Lösung mit Logging, Signalbehandlung und Timeout‑Steuerung. * **Komprimierte Datenübertragung** – Effiziente Payload‑Kompression für MQTT‑Nachrichten. +[NOTE] +.JSON Output Schema +==== +Decodierte Nachrichten werden als JSON-Objekte publiziert. Die Protokoll-Metadaten sind nun im Feld `protocol` enthalten, das `data`-Feld enthält die reinen, von der Protokoll-Preamble bereinigten Nutzdaten, und das `raw`-Feld enthält die unveränderte, rohe Nachrichtenzeichenkette der Firmware. + +include::./docs/architecture/snippets/json_output_schema_example.adoc[] +==== + == Demo === MQTT-CLI-Integration diff --git a/docs/01_user_guide/mqtt_api.adoc b/docs/01_user_guide/mqtt_api.adoc index 706b502..670bd7b 100644 --- a/docs/01_user_guide/mqtt_api.adoc +++ b/docs/01_user_guide/mqtt_api.adoc @@ -36,6 +36,49 @@ Alle nachfolgenden Beispiele verwenden `signalduino/v1` als Basis. | Dekodierte Funknachrichten. |=== +=== Empfangene Nachrichten (Output-Schema) + +Dekodierte Funksignale werden auf dem Topic `signalduino/v1/state/messages` publiziert. + +Die Payload folgt dem neuen, strukturierten Schema, bei dem protokollspezifische Metadaten im Feld `protocol` enthalten sind, das `data`-Feld die reinen, von der Protokoll-Preamble bereinigten Nutzdaten enthält, und das `raw`-Feld die unveränderte, rohe Nachrichtenzeichenkette der Firmware. + +[source,json] +---- +{ + "data": "30E0A1AA4241DE6C000200000BC5", + "protocol_id": "125", + "raw": "MS;P0=-32001;P1=488;D=0101;CP=1;R=48;", + "metadata": { + "rssi": -74, + "freq_afc": 123 + }, + "protocol": { + "name": "WH31", + "id": "125", + "preamble": "W125#", + "format": "2-FSK", + "clock": 17257 + } +} +---- + +==== Detailierte Felder der Output-Nachricht + +[cols="1,3", options="header"] +|=== +| Feld | Beschreibung +| `data` +| Der dekodierte, bereinigte Payload (Hex- oder Bit-String), *ohne* Protokoll-Preamble (z.B. `W125#`). +| `protocol_id` +| Die numerische oder alphanumerische ID des erkannten Protokolls. +| `raw` +| Die ursprüngliche, unmodifizierte Nachrichtenzeichenkette (`string`), die von der Firmware empfangen wurde (z.B. `MS;P0=-32001;P1=488;D=0101;CP=1;R=48;`). +| `metadata` +| Allgemeine gerätespezifische Metadaten (z.B. `rssi`, `freq_afc`). +| `protocol` +| Ein Dictionary mit spezifischen Protokoll-Details, wie `name`, `id`, `preamble`, `format` und dem verwendeten `clock`-Wert. +|=== + === Request- und Response-Format Alle Requests verwenden das folgende JSON-Format. Für einfache Befehle (meiste GETs) kann die Payload einfach `{}` sein. diff --git a/docs/03_protocol_reference/protocol_details.adoc b/docs/03_protocol_reference/protocol_details.adoc index e111ef7..0831d78 100644 --- a/docs/03_protocol_reference/protocol_details.adoc +++ b/docs/03_protocol_reference/protocol_details.adoc @@ -6,6 +6,27 @@ PySignalduino unterstützt eine Vielzahl von Funkprotokollen im 433 MHz und 868 Die Datei `sd_protocols/protocols.json` ist die definitive Quelle für alle Protokollparameter (Timings, Preambles, Methoden). +=== Dekodiertes Nachrichtenformat + +Die dekodierten Nachrichten sind standardisierte JSON-Objekte, die protokollspezifische Metadaten im Feld `protocol` bereitstellen, den bereinigten Daten-Payload im Feld `data` (ohne Protokoll-Preamble), sowie die unveränderte, rohe Nachrichtenzeichenkette der Firmware im Feld `raw`. + +[source,json] +---- +{ + "data": "30E0A1AA4241DE6C000200000BC5", + "protocol_id": "125", + "protocol": { + "name": "WH31", + "id": "125", + "preamble": "W125#", + "format": "2-FSK", + "clock": 17257 + }, + "metadata": { ... }, + "raw": "MS;P0=-32001;P1=488;D=0101;CP=1;R=48;" +} +---- + == Auszug unterstützter Protokolle * **ID 10:** Oregon Scientific v2/v3 (Manchester, 433 MHz) diff --git a/docs/architecture/decisions/ADR-006-json-output-schema.adoc b/docs/architecture/decisions/ADR-006-json-output-schema.adoc new file mode 100644 index 0000000..19b1eab --- /dev/null +++ b/docs/architecture/decisions/ADR-006-json-output-schema.adoc @@ -0,0 +1,88 @@ += ADR-006: JSON Output Schema Refinement +:toc: macro +:toc-title: Inhaltsverzeichnis + +== Status +* **Status:** Proposed +* **Datum:** 2026-01-11 +* **Architecture Owner:** Roo (Architect) + +== Kontext +Das bestehende JSON-Output-Schema für decodierte Funksignale (von `DecodedMessage` abgeleitet) enthält die Protokoll-Preamble (z.B. `W125#`) als Präfix im Feld `payload`. Dies erschwert die automatische Verarbeitung durch Downstream-Systeme (wie MQTT-Clients oder andere FHEM-Module), da diese die Preamble manuell entfernen müssen, um an die reinen Nutzdaten zu gelangen. Des Weiteren fehlt eine standardisierte Möglichkeit, protokollspezifische Metadaten (wie Protokollname, Format, Taktfrequenz) im Output bereitzustellen, ohne diese in der allgemeinen `metadata`-Struktur zu verstecken. + +== Entscheidung +Wir werden das JSON-Output-Schema von `DecodedMessage` wie folgt anpassen: +1. **Nutzdaten-Bereinigung:** Die Protokoll-Preamble wird aus dem Nutzdatenfeld entfernt. Dieses Feld enthält nur noch die vom Protokolldecoder erzeugten reinen Daten (Hex- oder Bit-String). +2. **Hinzufügen des `protocol` Feldes:** Ein neues Feld `protocol` vom Typ `dict` wird zur `DecodedMessage` hinzugefügt, um strukturierte, protokollspezifische Informationen zu enthalten. + +Die Umbenennung des Nutzdatenfeldes von `payload` zu `data` sowie die Einführung des Feldes `raw` (für die ursprüngliche Nachricht) sind in link:ADR-007-data-and-raw-fields.adoc[ADR-007] dokumentiert, das dieses Schema ergänzt und präzisiert. Dieses ADR dient als Grundlage für die Einführung des `protocol`-Feldes und die Bereinigung des Nutzdateninhalts. + +=== Details zur neuen Struktur (Präzisiert durch ADR-007) + +[cols="1,1,4"] +|=== +| Feld | Typ | Beschreibung + +| `data` +| `str` +| Die bereinigten Nutzdaten (meist Hex-String) **ohne** Preamble/Postamble. Repräsentiert die protokollspezifischen Daten (ersetzt `payload`). + +| `raw` +| `str` +| Die ursprüngliche, unveränderte Nachricht vom Signalduino (z.B. `MU;...`). Dient der vollständigen Nachvollziehbarkeit. + +| `protocol` +| `object` +| Container für strukturierte Metadaten der erfolgreichen Protokolldemodulation. + +| `protocol.id` +| `str` +| Die interne ID des Protokolls (z.B. `125`). + +| `protocol.name` +| `str` +| Der menschenlesbare Name des Protokolls (aus `protocols.json`). + +| `protocol.preamble` +| `str` +| Die Preamble (z.B. `W125#`) des erkannten Protokolls. + +| `protocol.encoding` +| `str` +| Das Codierungsformat des Signals (z.B. `manchester`, `twostate`, `pulse`). + +| `protocol.clock` +| `float` +| Der Takt-Wert in Mikrosekunden (`us`), der für die Demodulation verwendet wurde. + +| `protocol.modulation` +| `str` +| (Optional) Modulationsart (z.B. `2-FSK`, `GFSK`) für FSK-Protokolle. + +| `protocol.bitlength` +| `int` +| (Optional) Die tatsächliche Bitlänge der `data`-Nutzdaten, falls vom Protokoll bekannt/erzwungen. Unterstützt z.B. die Grothe-Constraint-Prüfung. + +| `protocol.repeats` +| `int` +| (Optional) Die Anzahl der erkannten Wiederholungen dieses Pakets, relevant für Duplikaterkennung. +|=== + +=== Beispiel für die Datenstruktur (Präzisiert durch ADR-007) + +include::../snippets/json_output_schema_example.adoc[] + +== Konsequenzen +**Positiv:** +* Das `data`-Feld ist jetzt "sauber" und enthält nur die Nutzdaten. +* Protocolspezifische Metadaten sind standardisiert im `protocol`-Feld abrufbar. +* Vereinfacht die Integration mit Systemen, die strukturierte Daten erwarten. +* Das neue `raw`-Feld (siehe link:ADR-007-data-and-raw-fields.adoc[ADR-007]) ermöglicht besseres Debugging und vollständige Nachvollziehbarkeit. + +**Negativ:** +* Dies ist ein **Breaking Change** für alle existierenden Konsumenten des `DecodedMessage`-Outputs, die darauf angewiesen sind, dass die Preamble im `payload` enthalten ist. +* Alle Demodulations- und Parser-Logik muss angepasst werden, um die Preamble separat zu behandeln und das `protocol`-Feld sowie das neue `raw`-Feld zu befüllen. + +== Alternativen +1. **Preamble in `metadata` verschieben:** Hätte den Nutzdatenfeld gereinigt, aber die Protokolldetails weiterhin unstrukturiert gelassen. Abgelehnt, da ein dediziertes `protocol`-Feld die semantische Klarheit verbessert. +2. **Beibehaltung der alten Struktur:** Hätte Abwärtskompatibilität gewährleistet, aber die Notwendigkeit für eine Nutzdatenreinigung durch jeden Konsumenten beibehalten. Abgelehnt, da die verbesserte Struktur die Wartbarkeit und zukünftige Erweiterbarkeit deutlich erhöht. diff --git a/docs/architecture/decisions/ADR-007-data-and-raw-fields.adoc b/docs/architecture/decisions/ADR-007-data-and-raw-fields.adoc new file mode 100644 index 0000000..8d64201 --- /dev/null +++ b/docs/architecture/decisions/ADR-007-data-and-raw-fields.adoc @@ -0,0 +1,46 @@ += ADR-007: Renaming Payload to Data and Adding Raw Field +:doctype: article +:encoding: utf-8 +:lang: de +:status: Proposed +:decided-at: 2026-01-11 +:decided-by: Roo (Architect) + +[#adr-context] +== Kontext + +Im Zuge der Definition des neuen JSON-Output-Schemas (siehe ADR-006) wurde Feedback gesammelt, das zwei wesentliche Verbesserungen vorschlägt: + +. **Ambiguität des Feldes `payload`:** Der Begriff `payload` wird in MQTT- und Messaging-Kontexten häufig für den gesamten Nachrichteninhalt verwendet. Die Verwendung von `payload` für die spezifischen, decodierten Hex-Daten kann daher zu Verwirrung führen. +. **Bedarf an Rohdaten:** Für Debugging-Zwecke und fortgeschrittene Analysen ist es notwendig, Zugriff auf die ursprüngliche, unveränderte Nachricht zu haben, wie sie vom Signalduino-Gerät empfangen wurde (z.B. der komplette `MU;...`- oder `MC;...`-String), bevor irgendeine Verarbeitung oder Parsing stattgefunden hat. + +[#adr-decision] +== Entscheidung + +Wir passen das in ADR-006 definierte Schema wie folgt an: + +. **Umbenennung `payload` zu `data`:** + Das Feld, das bisher `payload` hieß und die bereinigten Hex- oder Binärdaten (ohne Preamble) enthielt, wird in **`data`** umbenannt. + +. **Einführung des Feldes `raw`:** + Es wird ein neues Feld **`raw`** auf der obersten Ebene des JSON-Objekts eingeführt. Dieses Feld enthält den ursprünglichen Nachrichten-String, der an den Parser übergeben wurde. + +=== Aktualisierte Datenstruktur + +include::../snippets/json_output_schema_example.adoc[] + +[#adr-consequences] +== Konsequenzen + +=== Positive Konsequenzen +* **Klarere Semantik:** `data` ist ein neutralerer Begriff für den Dateninhalt und vermeidet die Überladung des Begriffs `payload`. +* **Verbesserte Debugging-Möglichkeiten:** Durch das `raw`-Feld können Entwickler und User jederzeit nachvollziehen, was genau vom Gerät empfangen wurde, und Parser-Fehler leichter diagnostizieren. +* **Vollständigkeit:** Keine Information geht verloren; sowohl die Rohdaten als auch die interpretierten Daten stehen zur Verfügung. + +=== Negative Konsequenzen +* **Breaking Change:** Dies ändert das gerade in ADR-006 vorgeschlagene Schema. Da ADR-006 jedoch noch neu ist, sollte der Anpassungsaufwand gering sein. Code, der bereits `payload` verwendet, muss auf `data` umgestellt werden. + +[#adr-alternatives] +== Alternativen +* **Beibehaltung von `payload`:** Wurde verworfen, um die Ambiguität aufzulösen. +* **`raw` als Objekt:** Es wurde erwogen, `raw` strukturierter zu gestalten, aber für die einfache Nachvollziehbarkeit ist der originale String am wertvollsten. diff --git a/docs/architecture/proposals/fhem_mqtt_integration.adoc b/docs/architecture/proposals/fhem_mqtt_integration.adoc index f90bd0e..f2a6139 100644 --- a/docs/architecture/proposals/fhem_mqtt_integration.adoc +++ b/docs/architecture/proposals/fhem_mqtt_integration.adoc @@ -2,21 +2,34 @@ Dieses Dokument skizziert 3 Optionen zur Integration der über MQTT publizierten JSON-Nachrichten von PySignalDuino in ein FHEM-System, das traditionell String-basierte Nachrichten via `Dispatch()` erwartet. -Das Quell-Topic ist: `signalduino/v1/state/messages` (oder ähnlich, basierend auf der Konfiguration des Basis-Topics in PySignalDuino), mit einem JSON-Payload, der mindestens `id` (Protokoll-ID) und `data` (dekodierte Hex-Payload) enthält. +Das Quell-Topic ist: `signalduino/v1/state/messages` (oder ähnlich, basierend auf der Konfiguration des Basis-Topics in PySignalDuino), mit einem JSON-Payload, der `protocol_id`, `preamble` (Protokoll-Präambel, z.B. `W126#`) und `payload` (Daten-Payload) enthält. == Lösungsoptionen === Option 1: MQTT2_DEVICE + Perl-Mapping (`json2nameValue` in `attr`) -Diese Option nutzt die Standardfunktionalität des FHEM-Moduls `MQTT2_DEVICE` in Verbindung mit einem `attr` (Attribut), um das JSON-Payload zu parsen und spezifische Readings zu erstellen. +Diese Option nutzt die Standardfunktionalität des FHEM-Moduls `MQTT2_DEVICE` in Verbindung mit einer Perl-Hilfsfunktion, um das JSON-Payload zu parsen und über den zentralen `Dispatch`-Mechanismus an die logischen Module weiterzuleiten. + +**Implementierungs-Detail (POC):** +Es wurde eine Hilfsfunktion `MqttSignalduino_DispatchFromJSON($$)` in `99_MyUtils.pm` erstellt. + +* **Funktion:** Nimmt den JSON-String (vom MQTT-Event) und den Gerätenamen entgegen. +* **Logik:** Dekodiert JSON, kombiniert `preamble` (z.B. `W126#`) und `payload` (z.B. `4001...`) und ruft `Dispatch()` mit dem Geräte-Hash auf. +* **FHEM-Integration:** Aufruf erfolgt z.B. über `userReadings` oder `notify`. + +[source,perl] +---- +# Beispielaufruf in FHEM (z.B. userReadings oder notify) +{ MqttSignalduino_DispatchFromJSON($EVENT, $NAME) } +---- [cols="1,3"] |=== |Kriterium | Beschreibung -|**Vorteile** | Keine neue Modul-Entwicklung in FHEM nötig. Reine Konfiguration. Nutzt FHEM-Bordmittel. Geringste Abhängigkeiten. -|**Nachteile** | FHEM-interne `Dispatch()`-Logik wird umgangen. Es muss für jeden Sensortyp ein eigenes FHEM-Device angelegt werden, das die Readings direkt von MQTT liest. Die existierenden Module wie link:../../../.devcontainer/fhem-data/FHEM/14_SD_WS.pm[`14_SD_WS.pm`] (die auf `Dispatch()` warten) können nicht direkt verwendet werden. -|**Implementierungsaufwand (grob)** | **Niedrig**. Konfiguration von einem `MQTT2_DEVICE` und Erstellung der `attr` mit Perl-Code zum Parsen/Dispatch. -|**Notwendige Änderungen** | **FHEM:** Ein `MQTT2_DEVICE` muss abonniert und konfiguriert werden. Wichtig: Das Attribut `Clients` muss manuell gesetzt werden (z.B. `:SD_WS:SD_...`), damit `Dispatch` die Module findet. Es ist ein Perl-Code-Snippet in den Attributen erforderlich, um den `data`-String aus dem JSON-Objekt in das erwartete Format zu transformieren und dann die Readings zu setzen. +|**Vorteile** | Keine volle Modul-Entwicklung nötig. Nutzt FHEM-Bordmittel (`99_MyUtils.pm`). Ermöglicht die Wiederverwendung von `Dispatch()` und bestehenden logischen Modulen. +|**Nachteile** | Erfordert manuelle Installation der Funktion in `99_MyUtils.pm`. Das `MQTT2_DEVICE` muss korrekt konfiguriert sein (z.B. `Clients` Attribut). +|**Implementierungsaufwand (grob)** | **Niedrig**. Erstellung der Perl-Funktion und einmalige Einrichtung. +|**Notwendige Änderungen** | **FHEM:** Ein `MQTT2_DEVICE` muss konfiguriert werden. Die Funktion `MqttSignalduino_DispatchFromJSON` muss in `99_MyUtils.pm` verfügbar sein. |=== @@ -30,7 +43,7 @@ Diese Option nutzt `MQTT2_DEVICE` zum Empfang des JSONs und lagert die Parsing- |**Vorteile** | Sauberer Code, aber manuelle Integration nötig. Das `MQTT2_DEVICE` wird minimalistisch gehalten. Erlaubt die Wiederverwendung von `Dispatch()`. |**Nachteile** | Erfordert Installation einer `.pm`-Datei (ähnlich wie Option 2, aber kein volles Modul). Erfordert einen `notify` oder einen `userReadings`-Aufruf, um die Logik auszuführen. |**Implementierungsaufwand (grob)** | **Niedrig bis Mittel**. Erstellung einer `.pm`-Utility-Datei mit einer Pars- und Dispatch-Funktion. Konfiguration eines `MQTT2_DEVICE`. -|**Notwendige Änderungen** | **FHEM:** Erstellung einer `.pm`-Datei mit einer Utility-Funktion (z.B. `SDU_DispatchJSON($$)`). Diese Funktion wird direkt in der `readingList` des `MQTT2_DEVICE` aufgerufen. Wichtig: Das Attribut `Clients` muss am `MQTT2_DEVICE` manuell gepflegt werden. **PySignalDuino:** Keine Änderungen. +|**Notwendige Änderungen** | **FHEM:** Erstellung einer `.pm`-Datei mit einer Utility-Funktion (z.B. `SDU_DispatchJSON($$)`). Diese Funktion wird direkt in der `readingList` des `MQTT2_DEVICE` aufgerufen. Wichtig: Das Attribut `Clients` muss am `MQTT2_DEVICE` manuell gepflegt werden. **PySignalDuino:** Die Payload-Generierung liefert `preamble` und `payload` separat im JSON. |=== === Option 2: Eigenes FHEM-Modul (PySignalDuino-Bridge) @@ -41,22 +54,22 @@ Ein neues FHEM-Modul, das die MQTT-Nachrichten von PySignalDuino abonniert und i |=== |Kriterium | Beschreibung |**Vorteile** | **Beste Kompatibilität**. Ermöglicht die Wiederverwendung aller bestehenden FHEM-Module (z.B. link:../../../.devcontainer/fhem-data/FHEM/14_SD_WS.pm[`14_SD_WS.pm`]), da die Bridge das ursprüngliche `Dispatch()`-Verhalten emuliert. Trennung von PySignalDuino-Logik (JSON) und FHEM-Logik (String-Dispatch). -|**Nachteile** | Erfordert die Entwicklung, Wartung und Installation eines neuen Perl-Moduls in FHEM. Komplexität in der JSON-zu-String-Konvertierung (Mapping der Protokoll-ID auf den String-Präfix, z.B. ID 49 auf "W49#..."). +|**Nachteile** | Erfordert die Entwicklung, Wartung und Installation eines neuen Perl-Moduls in FHEM. |**Implementierungsaufwand (grob)** | **Mittel**. Entwicklung des Bridge-Moduls in Perl, das die MQTT-Subscription und die JSON-Parsing/Dispatch-Logik implementiert. -|**Notwendige Änderungen** | **FHEM:** Neues Perl-Modul (z.B. `98_PySignalDuinoBridge.pm` oder `00_PySignalDuinoBridge.pm`) muss erstellt werden, das den JSON-Payload parst und basierend auf der Protokoll-ID den FHEM-kompatiblen String generiert (z.B. `P#` oder `W#`). **PySignalDuino:** Keine Änderungen. +|**Notwendige Änderungen** | **FHEM:** Neues Perl-Modul (z.B. `98_PySignalDuinoBridge.pm` oder `00_PySignalDuinoBridge.pm`) muss erstellt werden, das den JSON-Payload empfängt, `preamble` und `payload` verknüpft. |=== === Option 3: Anpassung in PySignalDuino (FHEM-Mode) -PySignalDuino würde eine neue Konfigurationsoption erhalten, die es ihm erlaubt, *zusätzlich* zu oder *anstelle* des Standard-JSON-Formats die traditionellen, von FHEM erwarteten Strings zu publizieren. +PySignalDuino wurde bereits angepasst, um die Felder `preamble` und `payload` zu senden, die zusammen dem FHEM-Dispatch-Format entsprechen. [cols="1,3"] |=== |Kriterium | Beschreibung -|**Vorteile** | Höchste Performance (keine Parsing/Konvertierung in FHEM). Direkte Wiederverwendung der `00_SIGNALduino.pm` (oder `MQTT2_DEVICE` mit einfacher Regex-Subscription) zur Übergabe der Strings an `Dispatch()`. -|**Nachteile** | **Verletzung des Architekturprinzips** (AGENTS.md: Architecture-First Development Process). PySignalDuino sollte eine reine Bridge sein und das standardisierte JSON-Format beibehalten. Ein FHEM-spezifisches Ausgabeformat erhöht die Wartungslast und die Kopplung. -|**Implementierungsaufwand (grob)** | **Mittel**. Änderung der Python-Logik (in z.B. link:../../../signalduino/mqtt.py[`signalduino/mqtt.py`]) zur String-Formatierung basierend auf der Protokoll-ID. -|**Notwendige Änderungen** | **PySignalDuino:** Implementierung der FHEM-String-Konvertierungslogik. Neue Umgebungsvariable (z.B. `MQTT_FHEM_MODE=true`). **FHEM:** Es kann der vorhandene `MQTT2_DEVICE` oder eine geringfügig angepasste Version von link:../../../.devcontainer/fhem-data/FHEM/00_SIGNALduino.pm[`00_SIGNALduino.pm`] verwendet werden, um den String direkt zu abonnieren und zu dispatchen. +|**Vorteile** | Flexibles Format (JSON), das sowohl FHEM als auch andere Systeme unterstützt. +|**Nachteile** | **Teilweise realisiert.** PySignalDuino sendet jetzt standardmäßig das Format mit `preamble` und `payload` im JSON. +|**Implementierungsaufwand (grob)** | **Erledigt**. Änderung in `signalduino/mqtt.py` ist bereits erfolgt. +|**Notwendige Änderungen** | **PySignalDuino:** `signalduino/mqtt.py` sendet `preamble` (z.B. `W126#`) und `payload` (Daten) separat. **FHEM:** Kombination von `preamble` und `payload` vor dem Dispatch. |=== === Option 4: Portierung der Dekodier-Logik (Client-Module) nach PySignalDuino @@ -101,7 +114,7 @@ Um die verschiedenen Optionen besser visualisieren zu können, folgen hier Code- [source,perl] ---- signalduino/v1/state/messages:.* { json2nameValue($EVENT, 'sd_') } -sd_data:.* { my ($id, $data) = ($EVTPART0 =~ /sd_id_(\d+) /g, $EVTPART0 =~ /sd_data_([0-9A-F]+)/g);; if($data) { Dispatch('signalduino', "P$id\#$data") } } +sd_payload:.* { my $pre = ReadingsVal($NAME, "sd_preamble", ""); my $pay = ReadingsVal($NAME, "sd_payload", ""); Dispatch('signalduino', $pre . $pay) } ---- **Flussdiagramm:** @@ -113,11 +126,12 @@ sequenceDiagram participant F as FHEM (MQTT2_DEVICE) participant D as FHEM (Sensor Device) - P ->> M: Publish JSON Payload + P ->> M: Publish JSON Payload (mit "preamble": "W126#", "payload": "40...") M ->> F: MQTT Message F ->> F: readingList: json2nameValue() -> Readings F ->> F: userReadings (optional): Trigger Dispatch() - F ->> F: Dispatch("P#") + F ->> F: Combine preamble + payload + F ->> F: Dispatch("W126#40...") F ->> D: Message an Dispatch() D ->> D: Handle Message .... @@ -132,11 +146,9 @@ sub SDU_DispatchJSON($$) { my ($hash, $json_payload) = @_; use JSON; my $data_hash = JSON::decode_json($json_payload); - my $id = $data_hash->{id}; - my $data = $data_hash->{data}; + my $msg = $data_hash->{preamble} . $data_hash->{payload}; - # Konvertierung und Dispatch - my $msg = "P" . $id . "\#" . $data; + # Dispatch Dispatch($hash, $msg); return 1; } @@ -162,8 +174,7 @@ sequenceDiagram P ->> M: Publish JSON Payload M ->> F: MQTT Message F ->> U: readingList: SDU_DispatchJSON($hash, $EVENT) - U ->> U: Parse JSON - U ->> U: Convert to "P#" + U ->> U: Parse JSON (combine "preamble" + "payload") U ->> F: Dispatch() F ->> D: Message an Dispatch() D ->> D: Handle Message @@ -180,10 +191,7 @@ sub Bridge_Parse($) { # ... JSON-Payload abrufen ... use JSON; my $data_hash = JSON::decode_json($json_payload); - my $id = $data_hash->{id}; - my $data = $data_hash->{data}; - - my $msg = "P" . $id . "\#" . $data; + my $msg = $data_hash->{preamble} . $data_hash->{payload}; # Aufruf des Standard-Dispatch return Dispatch($hash, $msg); @@ -201,8 +209,8 @@ sequenceDiagram P ->> M: Publish JSON Payload M ->> B: MQTT Message (Subscription) - B ->> B: Internal Parse/Convert Logic - B ->> D: Dispatch("P#") + B ->> B: Internal Parse (combine "preamble" + "payload") + B ->> D: Dispatch("W126#40...") D ->> D: Handle Message .... @@ -211,15 +219,17 @@ sequenceDiagram **PySignalDuino-Code (in link:../../../signalduino/mqtt.py[`signalduino/mqtt.py`]):** [source,python] ---- -# Wenn FHEM-Mode aktiv: -fhem_string = f"P{protocol_id}#{hex_data}" -mqtt_client.publish(fhem_topic, fhem_string) +# Implementiert: Preamble und Payload separat +# if self._protocol_handler: +# preamble = self._protocol_handler.check_property(message.protocol_id, 'preamble', '') +# message_dict["preamble"] = preamble +# message_dict["payload"] = message.payload.upper() ---- **FHEM-Konfiguration (MQTT2_DEVICE `readingList`):** [source,perl] ---- -signalduino/v1/state/messages:.* { Dispatch('signalduino', $EVENT) } +signalduino/v1/state/messages:.* { my $h=JSON::decode_json($EVENT); Dispatch('signalduino', $h->{preamble} . $h->{payload}) } ---- **Flussdiagramm:** @@ -231,10 +241,10 @@ sequenceDiagram participant F as FHEM (MQTT2_DEVICE) participant D as FHEM (Sensor Device) - P ->> P: Convert to "P#" String - P ->> M: Publish FHEM String Payload + P ->> P: Separate Preamble and Payload + P ->> M: Publish JSON Payload M ->> F: MQTT Message - F ->> D: readingList: Dispatch('signalduino', $EVENT) + F ->> D: readingList: Dispatch('signalduino', preamble + payload) D ->> D: Handle Message .... @@ -256,8 +266,9 @@ def decode_protocol_49(hex_data): # Im MQTT-Publisher: decoded_values = decode_protocol_49(data) payload = { - "id": 49, - "data": data, + "protocol_id": "49", + "preamble": "P49#", + "payload": data, "values": decoded_values # Neues Feld mit interpretierten Werten } mqtt_client.publish(topic, json.dumps(payload)) @@ -273,7 +284,7 @@ Dies erzeugt Readings wie `values_temperature` und `values_battery` direkt am De **Flussdiagramm:** [mermaid] -.... +---- sequenceDiagram participant P as PySignalDuino participant M as MQTT Broker @@ -290,7 +301,7 @@ sequenceDiagram M ->> H: MQTT Message H ->> H: Auto Discovery / Sensor Update end -.... +---- == Detaillierte Bewertung @@ -318,23 +329,22 @@ sequenceDiagram * Dies sollte das strategische Ziel sein, um PySignalDuino zu einem echten, universellen IoT-Gateway zu machen. * Es wird empfohlen, dies **schrittweise** für die populärsten Protokolle (z.B. IT, WS) umzusetzen, während die anderen Protokolle weiterhin über Option 2 (Bridge) an FHEM übergeben werden. -* Dies ermöglicht einen sanften Übergang: PySignalDuino liefert `data` (für die Bridge) UND `values` (wenn der Decoder schon portiert ist). +* Dies ermöglicht einen sanften Übergang: PySignalDuino liefert `payload` (für die Bridge) UND `values` (wenn der Decoder schon portiert ist). === Implementierungs-ToDos für Option 2 (Bridge-Modul) -1. **Erstellung des Bridge-Moduls:** Erstellen des FHEM Perl-Moduls (z.B. `98_PySignalDuinoBridge.pm` oder `00_PySignalDuinoBridge.pm`). -2. **MQTT-Abonnement:** Implementierung der Logik zur Subscription des Topics `signalduino/v1/state/messages` (oder konfiguriertem Topic). -3. **JSON-Parsing:** Implementierung der Perl-Logik zum Parsen des JSON-Payloads (z.g. mit `JSON::decode_json`). -4. **String-Konvertierung:** Implementierung der Logik, die `protocol_id` und `data` aus dem JSON-Objekt nimmt und den traditionellen String (z.B. `P#` oder `W#`) generiert. -5. **Dispatching:** Aufruf von `Dispatch()` innerhalb des Moduls, um die Nachricht an das FHEM-System weiterzugeben. -6. **Konfiguration und Tests:** Dokumentation und Test der FHEM-Konfiguration (Definieren der Bridge). +. **Erstellung des Bridge-Moduls:** Erstellen des FHEM Perl-Moduls (z.B. `98_PySignalDuinoBridge.pm` oder `00_PySignalDuinoBridge.pm`). +. **MQTT-Abonnement:** Implementierung der Logik zur Subscription des Topics `signalduino/v1/state/messages` (oder konfiguriertem Topic). +. **JSON-Parsing:** Implementierung der Perl-Logik zum Parsen des JSON-Payloads (z.g. mit `JSON::decode_json`). +. **Dispatching:** Extraktion von `payload` aus dem JSON und Aufruf von `Dispatch()`, um die Nachricht an das FHEM-System weiterzugeben. +. **Konfiguration und Tests:** Dokumentation und Test der FHEM-Konfiguration (Definieren der Bridge). == Geplanter Arbeitsablauf -1. **Phase 1 (Design/Planung):** Abschluss der Architekturanalyse und Erstellung des Plans (Abgeschlossen mit diesem Dokument). -2. **Phase 2 (Implementierung):** +. **Phase 1 (Design/Planung):** Abschluss der Architekturanalyse und Erstellung des Plans (Abgeschlossen mit diesem Dokument). +. **Phase 2 (Implementierung):** * Erstellung des FHEM-Bridge-Moduls. * Einrichtung der FHEM-Konfiguration für das Modul. -3. **Phase 3 (Validierung):** Testen der End-to-End-Kette (PySignalDuino publiziert JSON -> Bridge parst -> Bridge dismatched String -> FHEM Sensor-Device reagiert). +. **Phase 3 (Validierung):** Testen der End-to-End-Kette (PySignalDuino publiziert JSON -> Bridge parst -> Bridge dismatched String -> FHEM Sensor-Device reagiert). **Nächster Schritt:** Wechsel in den Code-Modus, um das FHEM-Bridge-Modul zu implementieren (oder mit der Implementierung zu beginnen). diff --git a/docs/architecture/proposals/json_schema_refinement.adoc b/docs/architecture/proposals/json_schema_refinement.adoc new file mode 100644 index 0000000..2f5ed95 --- /dev/null +++ b/docs/architecture/proposals/json_schema_refinement.adoc @@ -0,0 +1,88 @@ += Architecture Proposal: JSON Output Schema Refinement +:toc: macro +:toc-title: Inhaltsverzeichnis + +== Status +* **Status:** Proposed +* **Datum:** 2026-01-11 +* **Autor:** Roo (Architect) + +== Kontext +Das aktuelle JSON-Nachrichtenschema für decodierte Signale vermischt Protokoll-Metadaten (die Preamble) mit den eigentlichen Nutzdaten (Payload). Zudem fehlen strukturierte Informationen über das erkannte Protokoll, die für weiterverarbeitende Systeme (z.B. Home Assistant, FHEM via MQTT) nützlich wären. + +Aktuelles Beispiel: +[source,json] +---- +{ + "payload": "W125#30E0A1AA4241DE6C000200000BC5", + "metadata": { ... }, + "protocol_id": "125" +} +---- + +== Problemstellung +1. **Verschmutzter Payload:** Der Payload enthält die Preamble (z.B. `W125#`), was nachgelagerte Parser zwingt, diese zu entfernen. +2. **Fehlende Protokolldetails:** Es gibt kein dediziertes Feld, das alle relevanten statischen und dynamischen Eigenschaften des erkannten Protokolls zusammenfasst. + +== Lösungsvorschlag + +=== 1. Bereinigung des Payloads +Die Preamble wird aus dem `payload`-Feld entfernt. Der Payload enthält nur noch die reinen Daten (Hex-String). + +=== 2. Erweiterung um `protocol`-Objekt +Ein neues Feld `protocol` wird eingeführt, das strukturierte Informationen enthält. + +Neues Schema: +[source,json] +---- +{ + "payload": "30E0A1AA4241DE6C000200000BC5", + "protocol_id": "125", + "protocol": { + "name": "WH31", + "id": "125", + "preamble": "W125#", + "format": "manchester", + "clock": 17257, // Optional: erkannter oder definierter Takt + "modulation": "2-FSK" // Optional: aus Protokolldefinition + }, + "metadata": { + "rssi": -74, + "freq_afc": 123 + } +} +---- + +== Betroffene Komponenten + +=== `sd_protocols` +Die Mixins für die Demodulation müssen angepasst werden, um die Preamble nicht mehr an den Payload anzuhängen, sondern separat zurückzugeben oder verfügbar zu machen. + +* `sd_protocols/manchester.py`: `_demodulate_mc_data` +* `sd_protocols/message_synced.py`: `demodulate_ms` +* `sd_protocols/message_unsynced.py`: `demodulate_mu` +* `sd_protocols/sd_protocols.py`: `demodulate_mc`, `demodulate_mn` + +=== `signalduino/types.py` +Die Klasse `DecodedMessage` muss um das Feld `protocol` erweitert werden. + +[source,python] +---- +@dataclass(slots=True) +class DecodedMessage: + protocol_id: str + payload: str + raw: RawFrame + metadata: dict = field(default_factory=dict) + protocol: dict = field(default_factory=dict) # Neu +---- + +=== `signalduino/parser` +Die Parser-Klassen (`MCParser`, `MSParser`, `MUParser`, `MNParser`) müssen sicherstellen, dass das `protocol`-Feld korrekt aus den Rückgabewerten der Protokollschicht befüllt wird. + +== Migration +Dies ist eine **Breaking Change** für Konsumenten, die erwarten, dass die Preamble Teil des Payloads ist. Die Versionsnummer sollte entsprechend (Minor oder Major, je nach Versionierungsstrategie) erhöht werden. + +== Alternativen +* **Status Quo beibehalten:** Führt zu unnötigem Parsing-Aufwand bei jedem Client. +* **Preamble nur in Metadata:** Löst das Payload-Problem, bietet aber keine strukturierte Sicht auf das Protokoll. diff --git a/docs/architecture/snippets/json_output_schema_example.adoc b/docs/architecture/snippets/json_output_schema_example.adoc new file mode 100644 index 0000000..47c1fdc --- /dev/null +++ b/docs/architecture/snippets/json_output_schema_example.adoc @@ -0,0 +1,18 @@ +[source,json] +---- +{ + "data": "30E0A1AA4241DE6C000200000BC5", + "raw": "MC;LL=-1017;LH=932;...", + "metadata": { + "rssi": -74, + "freq_afc": 123 + }, + "protocol": { + "name": "WH31", + "id": "125", + "preamble": "W125#", + "format": "2-FSK", + "clock": 17257 + } +} +---- diff --git a/signalduino/mqtt.py b/signalduino/mqtt.py index 0b55a98..9090246 100644 --- a/signalduino/mqtt.py +++ b/signalduino/mqtt.py @@ -11,6 +11,12 @@ from .types import DecodedMessage, RawFrame from .persistence import get_or_create_client_id +# Import protocol loader helper to access preamble data +try: + from sd_protocols.loader import _protocol_handler +except ImportError: + _protocol_handler = None + class MqttPublisher: """Publishes DecodedMessage objects to an MQTT server and listens for commands.""" @@ -30,6 +36,7 @@ def __init__( self.client_id = get_or_create_client_id() self.client: Optional[mqtt.Client] = None # Will be set in __aenter__ self._listener_task: Optional[asyncio.Task[None]] = None # NEU: Task für den Command Listener + self._protocol_handler = _protocol_handler # Konfiguration: CLI/Args > ENV > Default self.mqtt_host = host or os.environ.get("MQTT_HOST", "localhost") @@ -224,8 +231,7 @@ async def _handle_command(self, command_name: str, payload: str) -> None: ) - @staticmethod - def _message_to_json(message: DecodedMessage) -> str: + def _message_to_json(self, message: DecodedMessage) -> str: """Serializes a DecodedMessage to a JSON string.""" # DecodedMessage uses dataclasses, but RawFrame inside it also uses a dataclass. @@ -240,8 +246,26 @@ def _raw_frame_to_dict(raw_frame: RawFrame) -> dict: message_dict["raw"] = _raw_frame_to_dict(message_dict["raw"]) # Remove empty or non-useful fields for publication - message_dict.pop("raw", None) # Do not publish raw frame data by default + # Note: 'raw' is now a string (ADR-007) and should be published. + # The pop operation (line 249 in original) is removed to include it. + + # Append preamble to data for FHEM compatibility (PreambleProtocolID#HexData) + preamble = "" + if self._protocol_handler: + try: + protocol_id = message.protocol.get('id') + if protocol_id: + # check_property returns the value or default + preamble = self._protocol_handler.check_property(protocol_id, 'preamble', '') + except Exception as e: + self.logger.warning("Failed to get preamble: %s", e) + + # Add new 'preamble' field + message_dict["preamble"] = preamble + # Ensure data (formerly payload) is uppercase + message_dict["data"] = message.data.upper() + return json.dumps(message_dict, indent=4) async def publish_simple(self, subtopic: str, payload: str, retain: bool = False) -> None: @@ -267,8 +291,7 @@ async def publish(self, message: DecodedMessage) -> None: topic = f"{self.base_topic}/state/messages" payload = self._message_to_json(message) await self.client.publish(topic, payload) - self.logger.debug("Published message for protocol %s to %s", message.protocol_id, topic) + protocol_id = message.protocol.get('id', 'N/A') + self.logger.debug("Published message for protocol %s to %s", protocol_id, topic) except Exception: self.logger.error("Failed to publish message", exc_info=True) - - \ No newline at end of file diff --git a/signalduino/parser/mc.py b/signalduino/parser/mc.py index 2fea412..6cf4643 100644 --- a/signalduino/parser/mc.py +++ b/signalduino/parser/mc.py @@ -75,7 +75,7 @@ def parse(self, frame: RawFrame) -> Iterable[DecodedMessage]: try: # Replace generic demodulate with MC-specific processing in the protocol layer # This call should now encapsulate the logic from SIGNALduino_Parse_MC (lines 2840-2919) - demodulated_list = self.protocols.demodulate_mc(msg_data, frame) + demodulated_list = self.protocols.demodulate_mc(msg_data, "MC") except Exception: self.logger.exception("Error during MC demodulation for line: %s", frame.line) return @@ -85,11 +85,30 @@ def parse(self, frame: RawFrame) -> Iterable[DecodedMessage]: self.logger.warning("Invalid result from demodulator: %s", decoded) continue + protocol_id_str = str(decoded["protocol_id"]) + + # Zugriff auf Protokolldaten über get_protocol_list() + protocol_data = self.protocols.get_protocol_list().get(protocol_id_str, {}) + raw_payload = str(decoded.get("payload", "")) + + # Protokollmetadaten extrahieren + protocol_meta: Dict[str, Any] = { + "id": protocol_id_str, + "name": protocol_data.get("name", f"Protocol_{protocol_id_str}"), + "format": protocol_data.get("format", ""), + "clock": protocol_data.get("clock", None), + "preamble": protocol_data.get("preamble", ""), + } + + # 1. Entferne die Preamble aus der Payload + preamble_len = len(protocol_meta["preamble"]) + payload = raw_payload[preamble_len:] + yield DecodedMessage( - protocol_id=str(decoded["protocol_id"]), - payload=str(decoded.get("payload", "")), - raw=frame, + data=payload, + raw=frame.line, metadata=decoded.get("meta", {}), + protocol=protocol_meta, ) def _parse_to_dict(self, line: str) -> Dict[str, Any]: diff --git a/signalduino/parser/mn.py b/signalduino/parser/mn.py index 3a3ce42..5f74b15 100644 --- a/signalduino/parser/mn.py +++ b/signalduino/parser/mn.py @@ -179,13 +179,13 @@ def parse(self, frame: RawFrame) -> Iterable[DecodedMessage]: self.logger.info("MN Parse: Decoded matched MN Protocol id %s dmsg=%s", pid, final_payload) yield DecodedMessage( - protocol_id=str(pid), - payload=final_payload, - raw=frame, + data=final_payload, + raw=frame.line, metadata={ "rssi": rssi, "freq_afc": freq_afc, "modulation": modulation, "rfmode": proto_rfmode }, + protocol={"id": str(pid), "model": "MN"} ) diff --git a/signalduino/parser/ms.py b/signalduino/parser/ms.py index d1a2114..ecc91c8 100644 --- a/signalduino/parser/ms.py +++ b/signalduino/parser/ms.py @@ -59,10 +59,10 @@ def parse(self, frame: RawFrame) -> Iterable[DecodedMessage]: continue yield DecodedMessage( - protocol_id=str(decoded["protocol_id"]), - payload=str(decoded.get("payload", "")), - raw=frame, + data=str(decoded.get("payload", "")), + raw=frame.line, metadata=decoded.get("meta", {}), + protocol={"id": str(decoded["protocol_id"]), "model": "MS"}, ) def _parse_to_dict(self, line: str) -> Dict[str, Any]: diff --git a/signalduino/parser/mu.py b/signalduino/parser/mu.py index e3f1583..baa04d3 100644 --- a/signalduino/parser/mu.py +++ b/signalduino/parser/mu.py @@ -72,11 +72,30 @@ def parse(self, frame: RawFrame) -> Iterable[DecodedMessage]: self.logger.warning("Invalid result from demodulator: %s", decoded) continue + protocol_id_str = str(decoded["protocol_id"]) + + # Korrektur: Zugriff auf Protokolldaten über get_protocol_list() + protocol_data = self.protocols.get_protocol_list().get(protocol_id_str, {}) + raw_payload = str(decoded.get("payload", "")) + + # Protokollmetadaten direkt aus dem Protokolldict extrahieren + protocol_meta: Dict[str, Any] = { + "id": protocol_id_str, + "name": protocol_data.get("name", f"Protocol_{protocol_id_str}"), # Fallback + "format": protocol_data.get("format", ""), + "clock": protocol_data.get("clock", None), + "preamble": protocol_data.get("preamble", ""), + } + + # 1. Entferne die Preamble aus der Payload + preamble_len = len(protocol_meta["preamble"]) + payload = raw_payload[preamble_len:] + yield DecodedMessage( - protocol_id=str(decoded["protocol_id"]), - payload=str(decoded.get("payload", "")), - raw=frame, + data=payload, + raw=frame.line, metadata=decoded.get("meta", {}), + protocol=protocol_meta, ) def _parse_to_dict(self, line: str) -> Dict[str, Any]: diff --git a/signalduino/types.py b/signalduino/types.py index 72d03e0..e96c90d 100644 --- a/signalduino/types.py +++ b/signalduino/types.py @@ -25,10 +25,10 @@ class RawFrame: class DecodedMessage: """Higher-level frame after running through the parser.""" - protocol_id: str - payload: str - raw: RawFrame + data: str + raw: str metadata: dict = field(default_factory=dict) + protocol: dict = field(default_factory=dict) @dataclass(slots=True) diff --git a/tests/test_controller.py b/tests/test_controller.py index 0e23f8a..6dee4af 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -194,7 +194,7 @@ async def test_send_command_timeout(mock_transport, mock_parser, mock_controller async def test_message_callback(mock_transport, mock_parser, mock_controller_initialize): """Test message callback invocation.""" callback_mock = Mock() - decoded_msg = DecodedMessage(protocol_id="1", payload="test", raw=RawFrame(line="")) + decoded_msg = DecodedMessage(data="test", raw="", protocol={"id": "1"}) mock_parser.parse_line.return_value = [decoded_msg] # Use side_effect to return the line once, then fall back to the fixture's yielding None diff --git a/tests/test_mc_parser.py b/tests/test_mc_parser.py index fb338ae..ff4358c 100644 --- a/tests/test_mc_parser.py +++ b/tests/test_mc_parser.py @@ -36,16 +36,41 @@ def mc_parser(mock_protocols, logger): ) def test_mc_parser_valid_message(mc_parser, mock_protocols, line, expected_protocol, expected_payload, expected_rssi): """Test valid MC messages.""" + + # Mock Protokolldaten + MOCKED_PROTOCOLS = { + "57": {"name": "FS20", "preamble": "W57#", "format": "HEX", "clock": 332}, + "119": {"name": "Funkbus", "preamble": "W119#", "format": "HEX", "clock": 342}, + "108": {"name": "Grothe", "preamble": "W108#", "format": "HEX", "clock": 500}, + } + mock_protocols.get_protocol_list.return_value = MOCKED_PROTOCOLS + + # Protokoll-Metadaten extrahieren + protocol_meta = MOCKED_PROTOCOLS.get(expected_protocol, {"preamble": ""}) + preamble = protocol_meta["preamble"] + + # Der Mock muss die Payload MIT Präambel zurückgeben + raw_payload_with_preamble = f"{preamble}{expected_payload}" + frame = RawFrame(line=line) - demodulated = [{"protocol_id": expected_protocol, "payload": expected_payload}] + demodulated = [{"protocol_id": expected_protocol, "payload": raw_payload_with_preamble}] mock_protocols.demodulate_mc.return_value = demodulated result = list(mc_parser.parse(frame)) mock_protocols.demodulate_mc.assert_called_once() assert len(result) == 1 - assert result[0].protocol_id == expected_protocol - assert result[0].payload == expected_payload + + # Neue/geänderte Assertions + assert result[0].protocol["id"] == expected_protocol + assert result[0].data == expected_payload # Erwartet die bereinigte Payload + assert result[0].raw == line + + # Protokoll-Metadaten Assertions + assert result[0].protocol["id"] == expected_protocol + assert result[0].protocol["name"] == protocol_meta["name"] + assert result[0].protocol["preamble"] == preamble + assert frame.rssi == expected_rssi diff --git a/tests/test_mn_bresser_lightning.py b/tests/test_mn_bresser_lightning.py index 8fcf92f..e49a806 100644 --- a/tests/test_mn_bresser_lightning.py +++ b/tests/test_mn_bresser_lightning.py @@ -30,8 +30,9 @@ def test_bresser_lightning_decoding(caplog): assert len(messages) == 1 msg = messages[0] - assert msg.protocol_id == expected_protocol_id - assert msg.payload == expected_payload + assert msg.protocol["id"] == expected_protocol_id + assert msg.data == expected_payload + assert msg.raw == line assert msg.metadata["rfmode"] == "Bresser_lightning" # 26000000 / 16384 * -2 / 1000 = -3.1738... -> rounded to -3.0 assert msg.metadata["freq_afc"] == -3.0 \ No newline at end of file diff --git a/tests/test_mn_parser.py b/tests/test_mn_parser.py index 01b25a4..7013599 100644 --- a/tests/test_mn_parser.py +++ b/tests/test_mn_parser.py @@ -110,7 +110,7 @@ def test_mn_parser_messages( if not raises_exception: assert len(result) == expected_message_count if result: - assert result[0].protocol_id == expected_protocol_id + assert result[0].protocol["id"] == expected_protocol_id else: assert not result else: @@ -202,7 +202,7 @@ def test_mn_parser_messages_perl_migration( if expected_message_count > 0: # Verify first message's protocol ID only if expected_protocol_id is set if expected_protocol_id is not None: - assert result[0].protocol_id == expected_protocol_id + assert result[0].protocol["id"] == expected_protocol_id # Verify freq_afc if expected if expected_freq_afc is not None: diff --git a/tests/test_mqtt.py b/tests/test_mqtt.py index d894014..bdf4714 100644 --- a/tests/test_mqtt.py +++ b/tests/test_mqtt.py @@ -26,20 +26,15 @@ def mock_controller(): @pytest.fixture def mock_decoded_message() -> DecodedMessage: return DecodedMessage( - protocol_id="1", - payload="RSL: ID=01, SWITCH=01, CMD=OFF", - raw=RawFrame( - line="+MU;...", - rssi=-80, - freq_afc=433.92, - message_type="MU", - ), + data="9374A400", + raw="MS;P1=1154;P2=-697;P3=559;P4=-1303;P5=-7173;D=351234341234341212341212123412343412341234341234343434343434343434;CP=3;SP=5;R=247;O;", metadata={ "protocol_name": "Conrad RSL v1", "message_hex": "AABBCC", "message_bits": "101010101011101111001100", "is_repeat": False, }, + protocol={"id": "1"}, # Hinzugefügtes Protokoll-ID-Feld ) @pytest.fixture @@ -134,8 +129,15 @@ async def test_mqtt_publisher_publish_success(MockClient, mock_decoded_message, assert isinstance(published_payload, str) payload_dict = json.loads(published_payload) - assert payload_dict["protocol_id"] == "1" - assert "raw" not in payload_dict # raw sollte entfernt werden + assert payload_dict["protocol"]["id"] == "1" + + # Payload sollte KEINE Preamble mehr enthalten, aber das neue Feld "preamble" schon + # Protocol 1 (Conrad RSL v1) hat Preamble "P1#" + assert payload_dict["data"] == "9374A400" + assert payload_dict["preamble"] == "P1#" + + # 'raw' sollte jetzt enthalten sein, da es ein String-Feld ist. + assert payload_dict["raw"] == "MS;P1=1154;P2=-697;P3=559;P4=-1303;P5=-7173;D=351234341234341212341212123412343412341234341234343434343434343434;CP=3;SP=5;R=247;O;" assert call_kwargs == {} # assert {} da keine kwargs im Code von MqttPublisher.publish übergeben werden assert "Published message for protocol 1 to test/signalduino/v1/state/messages" in caplog.text diff --git a/tests/test_ms_parser.py b/tests/test_ms_parser.py index a60045f..ee00018 100644 --- a/tests/test_ms_parser.py +++ b/tests/test_ms_parser.py @@ -57,5 +57,5 @@ def test_correct_mc_cul_tcm_97001(self, parser): assert len(results) > 0 # Optional: Check if it matched Protocol 0 - p0_match = any(r.protocol_id == '0' for r in results) + p0_match = any(r.protocol.get('id') == '0' for r in results) assert p0_match diff --git a/tests/test_mu_parser.py b/tests/test_mu_parser.py index 1eba1ae..069e449 100644 --- a/tests/test_mu_parser.py +++ b/tests/test_mu_parser.py @@ -20,15 +20,43 @@ def mu_parser(mock_protocols, logger): ) def test_mu_parser_valid_messages(mu_parser, mock_protocols, line, expected_protocol, expected_rssi): """Test valid MU messages.""" + + # Mock Protokolldaten + MOCKED_PROTOCOLS = { + "44": {"name": "IT-V1_V3", "preamble": "W44#", "format": "BITS", "clock": 250}, + "84": {"name": "Bresser-3_1", "preamble": "W84#", "format": "BITS", "clock": 330}, + } + mock_protocols.get_protocol_list.return_value = MOCKED_PROTOCOLS + + # Erwartete Payloads (Roh-Payload enthält Präambel, gereinigte Payload nicht) + if expected_protocol == "44": + raw_payload = "W44#123456" + expected_clean_payload = "123456" + expected_protocol_meta = MOCKED_PROTOCOLS["44"] + else: # expected_protocol == "84" + raw_payload = "W84#ABCDEF" + expected_clean_payload = "ABCDEF" + expected_protocol_meta = MOCKED_PROTOCOLS["84"] + frame = RawFrame(line=line) - demodulated = [{"protocol_id": expected_protocol}] + demodulated = [{"protocol_id": expected_protocol, "payload": raw_payload}] mock_protocols.demodulate.return_value = demodulated result = list(mu_parser.parse(frame)) mock_protocols.demodulate.assert_called_once() assert len(result) == 1 - assert result[0].protocol_id == expected_protocol + + # Neue/geänderte Assertions + assert result[0].protocol["id"] == expected_protocol + assert result[0].data == expected_clean_payload + assert result[0].raw == line + + # Protokoll-Metadaten Assertions + assert result[0].protocol["id"] == expected_protocol + assert result[0].protocol["name"] == expected_protocol_meta["name"] + assert result[0].protocol["preamble"] == expected_protocol_meta["preamble"] + # Correct the expected RSSI value for R=217 if expected_protocol == "84": assert frame.rssi == -93.5