diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b3833a0..08d3925 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,3 +1,4 @@ + // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/python { @@ -9,17 +10,9 @@ // Features to add to the dev container. More info: https://containers.dev/features. // "features": {}, "features": { - "ghcr.io/devcontainers/features/node:1": { - "nodeGypDependencies": true, - "installYarnUsingApt": true, - "version": "lts", - "pnpmVersion": "latest", - "nvmVersion": "latest" - }, - "ghcr.io/devcontainer-community/devcontainer-features/astral.sh-uv:1": { - "shellautocompletion": true, - "version": "latest" - } + "ghcr.io/devcontainers/features/node:1": {}, + "ghcr.io/devcontainer-community/devcontainer-features/astral.sh-uv:1": {}, + "ghcr.io/braun-daniel/devcontainer-features/asciidoc:1": {} //"ghcr.io/hspaans/devcontainer-features/pytest:2": {} }, // Use 'forwardPorts' to make a list of ports inside the container available locally. diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 34b5dc9..b0cc596 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -29,3 +29,18 @@ services: - ./mosquitto/log:/mosquitto/log command: mosquitto -c /mosquitto/config/mosquitto.conf restart: unless-stopped + + fhem: + image: ghcr.io/fhem/fhem-minimal-docker:5-bookworm + container_name: fhem-dev-server + ports: + - '38083:8083' # Expose FHEM web interface on port 38083 + environment: + - CONFIGTYPE=fhem_signalduino_example.cfg + - FHEM_PERM_DIR=0777 + - FHEM_PERM_FILES=0777 + volumes: + - ./fhem-data:/opt/fhem + depends_on: + - mqtt + restart: unless-stopped diff --git a/.devcontainer/fhem-data/fhem_signalduino_example.cfg b/.devcontainer/fhem-data/fhem_signalduino_example.cfg new file mode 100755 index 0000000..529f1fc --- /dev/null +++ b/.devcontainer/fhem-data/fhem_signalduino_example.cfg @@ -0,0 +1,86 @@ +attr global userattr cmdIcon devStateIcon:textField-long devStateStyle icon sortby webCmd webCmdLabel:textField-long widgetOverride +attr global autosave 0 +attr global logfile log/fhem-%Y-%m-%d.log +attr global modpath . +attr global nofork 0 +attr global pidfilename log/fhem.pid +attr global statefile ./log/fhem.save +attr global updateInBackground 1 +attr global verbose 3 +# FHEM Configuration for PySignalduino Dev Environment +# +# This file is loaded by the FHEM container via CONFIGTYPE environment variable. + + +# 1. Define FHEMWEB instance to access FHEM via Browser (Port 8083) +define WEB FHEMWEB 8083 global +setuuid WEB 695e9c21-f33f-c986-956a-7e26fc9adfc69728 +attr WEB editConfig 1 +attr WEB stylesheetPrefix dark + +# 2. Define Telnet for command line access (Optional) +#define telnetPort telnet 7072 global + +# 3. Basic Event Handling +define eventTypes eventTypes ./log/eventTypes.txt +setuuid eventTypes 695e9c21-f33f-c986-9ac3-190c47641a98acb9 + +# 4. Define the MQTT Client (Broker Connection) +# 'mqtt' is the hostname of the broker service in docker-compose.yml +define mqtt_broker MQTT2_CLIENT mqtt:1883 +setuuid mqtt_broker 695e9c21-f33f-c986-e617-d7301881c4685bc6 +attr mqtt_broker autocreate simple + +# 5. Define the SignalDuino MQTT Device +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) }\ + signalduino/v1/responses:.* { json2nameValue($EVENT, 'RESP_') }\ + signalduino/v1/errors:.* { json2nameValue($EVENT, 'ERR_') } +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) \ + version:noArg signalduino/v1/commands/get/system/version \ + freeram:noArg signalduino/v1/commands/get/system/freeram \ + uptime:noArg signalduino/v1/commands/get/system/uptime \ + # Decoder state commands \ + decoder_state:noArg signalduino/v1/commands/get/config/decoder \ + decoder_ms_enable:noArg signalduino/v1/commands/set/config/decoder_ms_enable \ + decoder_ms_disable:noArg signalduino/v1/commands/set/config/decoder_ms_disable \ + decoder_mu_enable:noArg signalduino/v1/commands/set/config/decoder_mu_enable \ + decoder_mu_disable:noArg signalduino/v1/commands/set/config/decoder_mu_disable \ + decoder_mc_enable:noArg signalduino/v1/commands/set/config/decoder_mc_enable \ + decoder_mc_disable:noArg signalduino/v1/commands/set/config/decoder_mc_disable \ + # CC1101 GET commands (noArg) \ + cc_config:noArg signalduino/v1/commands/get/cc1101/config \ + cc_patable_get:noArg signalduino/v1/commands/get/cc1101/patable \ + cc_register:noArg signalduino/v1/commands/get/cc1101/register \ + cc_freq_get:noArg signalduino/v1/commands/get/cc1101/frequency \ + cc_bandwidth_get:noArg signalduino/v1/commands/get/cc1101/bandwidth \ + cc_rampl_get:noArg signalduino/v1/commands/get/cc1101/rampl \ + cc_sensitivity_get:noArg signalduino/v1/commands/get/cc1101/sensitivity \ + cc_datarate_get:noArg signalduino/v1/commands/get/cc1101/datarate \ + cc_settings_get:noArg signalduino/v1/commands/get/cc1101/settings \ + cc_deviation_get:noArg signalduino/v1/commands/get/cc1101/deviation\ + # CC1101 SET commands \ + cc_frequency_set:textField signalduino/v1/commands/set/cc1101/frequency $EVTPART1 \ + cc_rampl_set:select,24,27,30,33,36,38,40,42 signalduino/v1/commands/set/cc1101/rampl $EVTPART1 \ + cc_sensitivity_set:select,4,8,12,16 signalduino/v1/commands/set/cc1101/sensitivity $EVTPART1 \ + cc_patable_set:select,-30_dBm,-20_dBm,-15_dBm,-10_dBm,-5_dBm,0_dBm,5_dBm,7_dBm,10_dBm signalduino/v1/commands/set/cc1101/patable $EVTPART1 \ + cc_bandwidth_set:textField signalduino/v1/commands/set/cc1101/bandwidth $EVTPART1 \ + cc_datarate_set:textField signalduino/v1/commands/set/cc1101/datarate $EVTPART1 \ + cc_deviation_set:textField signalduino/v1/commands/set/cc1101/deviation $EVTPART1 \ +\ + # Maintenance commands \ + factory_reset:noArg signalduino/v1/commands/set/factory_reset +attr PySignalDuino stateFormat state +# Map JSON payload to readings +# Define setter commands + +# Logfile for SignalDuino +define FileLog_PySignalDuino FileLog ./log/PySignalDuino-%Y.log PySignalDuino +setuuid FileLog_PySignalDuino 695e9c21-f33f-c986-1981-abe9a5a366b3c989 +attr FileLog_PySignalDuino logtype text +define Logfile FileLog /opt/fhem/log/fhem-%Y-%m-%d.log Logfile +setuuid Logfile 695e9c21-f33f-c986-cfda-4915c3e60c145721 diff --git a/.gitignore b/.gitignore index 81213fc..7e572d4 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,7 @@ SIGNALDuino-Firmware/ .devcontainer/.devcontainer.env .devcontainer/mosquitto/data/ .devcontainer/mosquitto/log/ +.devcontainer/fhem-data/* +!.devcontainer/fhem-data/fhem_signalduino_example.cfg + .roo/mcp.json diff --git a/docs/01_user_guide/mqtt_api.adoc b/docs/01_user_guide/mqtt_api.adoc index ce37a6f..616e2cf 100644 --- a/docs/01_user_guide/mqtt_api.adoc +++ b/docs/01_user_guide/mqtt_api.adoc @@ -140,8 +140,8 @@ GET-Befehle benötigen eine leere Payload (`{}`) oder nur eine `req_id`. | CC1101 PA-Tabelle. | `get/cc1101/register` -| `{"register_value": "C00 = 29"}` -| Liest den Wert eines einzelnen CC1101-Registers (Adresse 0x00). Der Befehl nimmt keinen Wert in der Payload entgegen und liest standardmäßig Register 0x00. +| `{"register_name": "IOCFG2", "address_hex": "00", "register_value": "C00 = 29"}` +| Liest den Wert eines einzelnen CC1101-Registers. *Erfordert den Registernamen* im `value`-Feld der Payload (z.B. `{"value": "IOCFG2"}`). | `get/cc1101/frequency` | `{"frequency": 868.3500}` @@ -303,3 +303,37 @@ Dieser Befehl sendet eine vorab encodierte Nachricht an das Signalduino-Gerät, | Nein | Optionale Frequenz in MHz (`F`). |=== + +[[_fhem_integration]] +== FHEM Integration + +PySignalduino lässt sich nahtlos in FHEM integrieren, indem ein MQTT-Broker als Vermittler genutzt wird. Die empfohlene Methode ist die Verwendung des FHEM-Moduls `MQTT2_CLIENT` zur Verbindung mit dem Broker und `MQTT2_DEVICE` zur Repräsentation des Signalduino. + +=== Beispielkonfiguration + +Eine vollständige Beispielkonfiguration finden Sie in der Datei `.devcontainer/fhem-data/fhem_signalduino_example.cfg`. Im DevContainer wird diese Datei automatisch als FHEM-Konfiguration geladen, sodass `PySignalDuino` sofort verfügbar ist. + +[source,fhem] +---- +# 1. Verbindung zum Broker herstellen (falls noch nicht vorhanden) +define mqtt_broker MQTT2_CLIENT mqtt:1883 +attr mqtt_broker autocreate simple + +# 2. PySignalduino Device definieren +define PySignalDuino MQTT2_DEVICE +attr PySignalDuino IODev mqtt_broker + +# 3. Readings für empfangene Nachrichten extrahieren +# Wandelt JSON-Payload automatisch in Readings um +attr PySignalDuino readingList signalduino/v1/state/messages:.* { json2nameValue($EVENT, '', $JSONMAP) } + +# 4. Senden von Befehlen ermöglichen +attr PySignalDuino setList raw:textField signalduino/v1/commands/set/raw $EVTPART1 \ +cc1101_reg:textField signalduino/v1/commands/set/cc1101_reg $EVTPART1 \ +version:noArg signalduino/v1/commands/get/system/version +---- + +=== Wichtige Hinweise + +* **Topics:** Stellen Sie sicher, dass das `readingList` Attribut dem in PySignalduino konfigurierten `MQTT_TOPIC` entspricht (Standard: `signalduino/v1/state/messages`). +* **JSON Parsing:** Die Funktion `json2nameValue` in FHEM ist ideal, um die flachen JSON-Objekte von PySignalduino direkt in FHEM Readings umzuwandeln. diff --git a/docs/architecture/proposals/fhem_mqtt_integration.adoc b/docs/architecture/proposals/fhem_mqtt_integration.adoc new file mode 100644 index 0000000..f90bd0e --- /dev/null +++ b/docs/architecture/proposals/fhem_mqtt_integration.adoc @@ -0,0 +1,340 @@ += PySignalDuino - FHEM Integrationsoptionen + +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. + +== 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. + +[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. +|=== + + +=== Option 1b: MQTT2_DEVICE + Perl-Bibliothek (Utils-Funktion) + +Diese Option nutzt `MQTT2_DEVICE` zum Empfang des JSONs und lagert die Parsing- und Dispatch-Logik in eine Utility-Bibliothek (eine `.pm`-Datei, die Funktionen exportiert) aus. Die Funktion dieser Bibliothek wird direkt über die `readingList` des `MQTT2_DEVICE` aufgerufen. + +[cols="1,3"] +|=== +|Kriterium | Beschreibung +|**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. +|=== + +=== Option 2: Eigenes FHEM-Modul (PySignalDuino-Bridge) + +Ein neues FHEM-Modul, das die MQTT-Nachrichten von PySignalDuino abonniert und intern die `Dispatch()`-Funktion von FHEM mit den traditionellen Signalduino-Strings aufruft. + +[cols="1,3"] +|=== +|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#..."). +|**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. +|=== + +=== 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. + +[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. +|=== + +=== Option 4: Portierung der Dekodier-Logik (Client-Module) nach PySignalDuino + +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. + +[cols="1,3"] +|=== +|Kriterium | Beschreibung +|**Vorteile** | **Perfekt für alle Konsumenten** (Home Assistant, Node-RED, FHEM, ioBroker), da keine proprietäre Dekodier-Logik im Zielsystem nötig ist. Die MQTT-Nachricht enthält direkt verwendbare Schlüssel-Wert-Paare (z.B. `{"temperature": 21.5, "humidity": 60, "battery": "ok"}`). Ermöglicht echtes "MQTT Auto Discovery" (z.B. für Home Assistant). Entkoppelt die Empfangslogik komplett von FHEM. +|**Nachteile** | **Extremer Aufwand.** Es müssen Dutzende/Hunderte von Protokoll-Dekodern von Perl nach Python portiert werden. Verschiebt die Komplexität massiv in dieses Projekt. +|**Implementierungsaufwand (grob)** | **Sehr Hoch**. Systematische Portierung der Logik aus diversen FHEM-Modulen nach Python. +|**Notwendige Änderungen** | **PySignalDuino:** Entwicklung einer Decoder-Schicht, die basierend auf Protokoll-ID den Hex-Payload interpretiert. Erweiterung des JSON-Payloads um dekodierte Felder. **FHEM:** `MQTT2_DEVICE` kann die JSON-Werte direkt als Readings übernehmen (kein Dispatch mehr nötig für diese Protokolle). +|=== + + +== Betrachtung anderer Automatisierungslösungen (Home Assistant, Node-RED) + +PySignalDuino publiziert die demodulierten Nachrichten in einem standardisierten JSON-Format über MQTT, was die Integration in andere Smart-Home-Systeme als FHEM erheblich vereinfacht. + +[cols="1,1,1,2"] +|=== +|Option | PySignalDuino-Payload | FHEM-spezifische Konsequenzen | Systemübergreifende Eignung (HA/Node-RED) +|**1, 1b, 2** | Standardisiertes JSON | Konvertierung/Parsing in FHEM notwendig. | **Gut.** Diese Systeme können JSON nativ und effizient parsen, benötigen aber eigene Logik (Templates/Scripts) zur Interpretation der Rohdaten, wenn FHEM dies nicht übernimmt. +|**3** | FHEM-spezifischer String (z.B. P49#...) | Direkte Integration in FHEM möglich. | **Schlecht.** HA/Node-RED müssten proprietäre FHEM-Strings parsen oder PySignalDuino müsste doppelt senden (JSON + String), was Bandbreite/Performance kostet. +|**4** | Dekodiertes JSON (Werte) | Keine Dispatch-Logik nötig; direkte Nutzung der Readings. | **Exzellent.** Der "Gold Standard". MQTT-Nachrichten sind selbsterklärend und sofort nutzbar. Ermöglicht Plug & Play. +|=== + +**Fazit:** +* Varianten, die das standardisierte JSON von PySignalDuino beibehalten (Option 1, 1b, 2), sind für eine Koexistenz gut geeignet. +* **Option 4** ist der klare Gewinner für moderne IoT-Landschaften (HA, Node-RED), erfordert aber den größten Aufwand in PySignalDuino. + + +== Grobe POC-Implementierungen + +Um die verschiedenen Optionen besser visualisieren zu können, folgen hier Code-Snippets und Diagramme. + +=== Option 1: MQTT2_DEVICE + Perl-Mapping (`json2nameValue` in `attr`) + +**FHEM-Konfiguration (MQTT2_DEVICE `readingList`):** +*(Hinweis: Das Attribut `Clients` muss am MQTT2_DEVICE manuell gesetzt werden, z.B. `:SD_WS:SD_...`)* +[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") } } +---- + +**Flussdiagramm:** +[mermaid] +.... +sequenceDiagram + participant P as PySignalDuino + participant M as MQTT Broker + participant F as FHEM (MQTT2_DEVICE) + participant D as FHEM (Sensor Device) + + P ->> M: Publish JSON Payload + M ->> F: MQTT Message + F ->> F: readingList: json2nameValue() -> Readings + F ->> F: userReadings (optional): Trigger Dispatch() + F ->> F: Dispatch("P#") + F ->> D: Message an Dispatch() + D ->> D: Handle Message +.... + +=== Option 1b: MQTT2_DEVICE + Perl-Bibliothek (Utils-Funktion) + +**FHEM-Utility-Datei (z.B. in `FHEM/MyUtils.pm` oder separater `.pm`-Datei):** +[source,perl] +---- +# In Utility-Datei +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}; + + # Konvertierung und Dispatch + my $msg = "P" . $id . "\#" . $data; + Dispatch($hash, $msg); + return 1; +} +---- + +**FHEM-Konfiguration (MQTT2_DEVICE `readingList`):** +*(Hinweis: Das Attribut `Clients` muss am MQTT2_DEVICE manuell gesetzt werden, z.B. `:SD_WS:SD_...`)* +[source,perl] +---- +signalduino/v1/state/messages:.* { SDU_DispatchJSON($hash, $EVENT) } +---- + +**Flussdiagramm:** +[mermaid] +.... +sequenceDiagram + participant P as PySignalDuino + participant M as MQTT Broker + participant F as FHEM (MQTT2_DEVICE) + participant U as FHEM Utility (.pm) + participant D as FHEM (Sensor Device) + + 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 ->> F: Dispatch() + F ->> D: Message an Dispatch() + D ->> D: Handle Message +.... + +=== Option 2: Eigenes FHEM-Modul (PySignalDuino-Bridge) + +**FHEM-Modul (Auszug aus `Bridge.pm`):** +[source,perl] +---- +sub Bridge_Parse($) { + my ($hash) = @_; + # ... MQTT-Message-Queue abrufen ... + # ... 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; + + # Aufruf des Standard-Dispatch + return Dispatch($hash, $msg); +} +---- + +**Flussdiagramm (Kapselung):** +[mermaid] +.... +sequenceDiagram + participant P as PySignalDuino + participant M as MQTT Broker + participant B as FHEM (Bridge Module) + participant D as FHEM (Sensor Device) + + P ->> M: Publish JSON Payload + M ->> B: MQTT Message (Subscription) + B ->> B: Internal Parse/Convert Logic + B ->> D: Dispatch("P#") + D ->> D: Handle Message +.... + +=== Option 3: Anpassung in PySignalDuino (FHEM-Mode) + +**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) +---- + +**FHEM-Konfiguration (MQTT2_DEVICE `readingList`):** +[source,perl] +---- +signalduino/v1/state/messages:.* { Dispatch('signalduino', $EVENT) } +---- + +**Flussdiagramm:** +[mermaid] +.... +sequenceDiagram + participant P as PySignalDuino + participant M as MQTT Broker + participant F as FHEM (MQTT2_DEVICE) + participant D as FHEM (Sensor Device) + + P ->> P: Convert to "P#" String + P ->> M: Publish FHEM String Payload + M ->> F: MQTT Message + F ->> D: readingList: Dispatch('signalduino', $EVENT) + D ->> D: Handle Message +.... + +=== Option 4: Portierung der Dekodier-Logik (Client-Module) nach PySignalDuino + +**PySignalDuino-Code (Erweiterung der Protokoll-Handler):** +[source,python] +---- +# Beispiel: Dekodierung eines Temperatursensors +def decode_protocol_49(hex_data): + # Portierte Logik aus 14_SD_WS.pm + temp_raw = int(hex_data[2:4], 16) + temp = (temp_raw - 50) / 10.0 + return { + "temperature": temp, + "battery": "ok" if hex_data[0] == 'A' else "low" + } + +# Im MQTT-Publisher: +decoded_values = decode_protocol_49(data) +payload = { + "id": 49, + "data": data, + "values": decoded_values # Neues Feld mit interpretierten Werten +} +mqtt_client.publish(topic, json.dumps(payload)) +---- + +**FHEM-Konfiguration (MQTT2_DEVICE):** +[source,perl] +---- +# Automatische Erstellung von Readings aus dem JSON +signalduino/v1/state/messages:.* { json2nameValue($EVENT) } +---- +Dies erzeugt Readings wie `values_temperature` und `values_battery` direkt am Device. + +**Flussdiagramm:** +[mermaid] +.... +sequenceDiagram + participant P as PySignalDuino + participant M as MQTT Broker + participant F as FHEM (MQTT2_DEVICE) + participant H as Home Assistant + + P ->> P: Demodulate Signal + P ->> P: Decode Values (Temp, Hum, etc.) + P ->> M: Publish JSON with "values" + par To FHEM + M ->> F: MQTT Message + F ->> F: Create Readings (Temp, Hum) + and To Home Assistant + M ->> H: MQTT Message + H ->> H: Auto Discovery / Sensor Update + end +.... + +== Detaillierte Bewertung + +[cols="1,1,1,1,1"] +|=== +|Feature | Option 1 (Mapping) | Option 1b (Utils) | Option 2 (Bridge-Modul) | Option 4 (Portierung) +|**Architektur** | "Bastel"-Lösung, unübersichtlich in Attributen | Sauberer Code, aber manuelle Integration nötig | Volle, standardkonforme FHEM-Integration | "Gold Standard", komplette Entkopplung +|**Dispatch-Fähigkeit** | **Kritisch:** Erfordert manuelles `Clients`-Attribut (fehleranfällig) | **Kritisch:** Erfordert manuelles `Clients`-Attribut | **Automatisch:** Integriert im Modul | Nicht nötig (Werte kommen direkt) +|**Konfigurationsaufwand** | Hoch (komplexe Regex im Attribut) | Mittel (Datei + Attribut) | Niedrig (ein `define`) | Sehr Niedrig (Auto-Create Readings) +|**Wartbarkeit** | Schlecht | Gut (Code ist versionierbar) | Sehr gut (gekapseltes Modul) | Sehr gut (zentral in Python) +|**Systemübergreifend** | Neutral (JSON bleibt erhalten) | Neutral (JSON bleibt erhalten) | Neutral (JSON bleibt erhalten) | **Exzellent** (Universal nutzbar) +|**Entwicklungsaufwand** | Niedrig | Mittel | Mittel | **Sehr Hoch** +|=== + +== Empfehlung + +**Kurzfristig / Mittelfristig:** +**Option 2: Eigenes FHEM-Modul (PySignalDuino-Bridge)** oder **Option 1b (Utils-Funktion)**. + +* Diese Optionen ermöglichen die sofortige Weiternutzung der existierenden, mächtigen FHEM-Module ohne riesigen Portierungsaufwand. +* Option 2 ist robuster für Endanwender ("Plug & Play"). Option 1b ist schneller für Entwickler umzusetzen. + +**Langfristig / Vision:** +**Option 4: Portierung der Dekodier-Logik nach PySignalDuino**. + +* 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). + +=== 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). + +== Geplanter Arbeitsablauf + +1. **Phase 1 (Design/Planung):** Abschluss der Architekturanalyse und Erstellung des Plans (Abgeschlossen mit diesem Dokument). +2. **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). + +**Nächster Schritt:** Wechsel in den Code-Modus, um das FHEM-Bridge-Modul zu implementieren (oder mit der Implementierung zu beginnen). diff --git a/signalduino/commands.py b/signalduino/commands.py index e42d050..12aaa8a 100644 --- a/signalduino/commands.py +++ b/signalduino/commands.py @@ -16,6 +16,21 @@ logger = logging.getLogger(__name__) +CC1101_REGISTER_MAP: Dict[str, int] = { + # Configuration Registers + "IOCFG2": 0x00, "IOCFG1": 0x01, "IOCFG0": 0x02, "FIFOTHR": 0x03, + "PKTLEN": 0x06, "PKTCTRL1": 0x07, "PKTCTRL0": 0x08, "ADDR": 0x09, + "CHANNR": 0x0A, "FSCTRL1": 0x0B, "FSCTRL0": 0x0C, "FREQ2": 0x0D, + "FREQ1": 0x0E, "FREQ0": 0x0F, "MDMCFG4": 0x10, "MDMCFG3": 0x11, + "MDMCFG2": 0x12, "MDMCFG1": 0x13, "MDMCFG0": 0x14, "DEVIATN": 0x15, + "MCSM1": 0x16, "MCSM0": 0x17, "FOCCFG": 0x19, "BSCFG": 0x1A, + "AGCCTRL2": 0x1B, "AGCCTRL1": 0x1C, "AGCCTRL0": 0x1D, "FSCAL3": 0x1F, + "FSCAL2": 0x20, "FSCAL1": 0x21, "FSCAL0": 0x22, "FSTEST": 0x23, + "PTEST": 0x25, "LTEST": 0x26, "PATABLE": 0x3E, + # Status Registers + "PARTNUM": 0x30, "VERSION": 0x31, "MARCSTATE": 0x35, "LQI": 0x38, "RSSI": 0x39 +} + # --- BEREICH 1: SignalduinoCommands (Implementierung der seriellen Befehle) --- class SignalduinoCommands: @@ -132,7 +147,7 @@ async def get_cc1101_settings(self, payload: Optional[Dict[str, Any]] = None) -> async def _read_register_value(self, register_address: int) -> int: """Liest einen CC1101-Registerwert und gibt ihn als Integer zurück.""" - response_dict = await self.read_cc1101_register(register_address) + response_dict = await self._read_cc1101_register_by_address(register_address) response = response_dict["register_value"] # Stellt sicher, dass wir nur den Wert nach 'C[A-Fa-f0-9]{2}\s*=\s*([0-9A-Fa-f]+)' extrahieren @@ -266,18 +281,41 @@ def _calculate_datarate_registers(self, datarate_kbaud: float) -> tuple[int, int return best_drate_e, best_drate_m - async def read_cc1101_register(self, register_address: int, timeout: float = SDUINO_CMD_TIMEOUT) -> Dict[str, str]: - """Read CC1101 register (C)""" + async def _read_cc1101_register_by_address(self, register_address: int, timeout: float = SDUINO_CMD_TIMEOUT) -> Dict[str, str]: + """Liest CC1101-Register über die numerische Adresse (C) und gibt die rohe Antwort zurück.""" hex_addr = f"{register_address:02X}" # Response-Pattern: ccreg 00: oder Cxx = yy (aus 00_SIGNALduino.pm, Zeile 87) - # Die Regex muss an den Anfang und das Ende der Zeile gebunden werden (re.match wird verwendet) - # ^(C[a-f0-9]{2}\s*=\s*[a-f0-9]+|ccreg 00:.*)\s*$ - # Hinweis: *Der Controller verwendet re.match*, was implizit ^ bindet. - # Wir müssen den Regex also an das Ende binden, um Leerzeichen zu erlauben. response_pattern = re.compile(r'^\s*(C[a-f0-9]{2}\s*=\s*[a-f0-9]+|ccreg [a-f0-9]{2}:.*)\s*$', re.IGNORECASE) response = await self._send_command(command=f"C{hex_addr}", expect_response=True, timeout=timeout, response_pattern=response_pattern) return {"register_value": response} + async def _read_cc1101_register_by_name(self, register_name: str, timeout: float = SDUINO_CMD_TIMEOUT) -> Dict[str, Any]: + """Internal: Read CC1101 register by name (e.g., 'IOCFG2').""" + register_name = register_name.upper() + register_address = CC1101_REGISTER_MAP.get(register_name) + if register_address is None: + raise CommandValidationError(f"Unknown CC1101 register name: {register_name}") + + response_dict = await self._read_cc1101_register_by_address(register_address, timeout) + + # Füge den Registernamen und die Adresse zur Antwort hinzu + response_dict["register_name"] = register_name + response_dict["address_hex"] = f"{register_address:02X}" + + return response_dict + + async def read_cc1101_register(self, payload: Dict[str, Any], timeout: float = SDUINO_CMD_TIMEOUT) -> Dict[str, Any]: + """ + MqttCommand: Reads a CC1101 register by name provided in the MQTT payload's 'value' field. + The name must be a key from CC1101_REGISTER_MAP. + """ + register_name = payload.get("value") + if not register_name: + raise CommandValidationError("Payload for read_cc1101_register must contain 'value' with the register name.") + + # Ruft die interne Methode auf + return await self._read_cc1101_register_by_name(str(register_name), timeout=timeout) + async def _get_frequency_registers(self) -> int: """Liest die CC1101 Frequenzregister (FREQ2, FREQ1, FREQ0) und kombiniert sie zu einem 24-Bit-Wert (F_REG).""" @@ -296,15 +334,15 @@ def extract_hex_value(response: str) -> int: raise ValueError(f"Unexpected response format for CC1101 register read: {response}") # FREQ2 (0D) - response2 = await self.read_cc1101_register(FREQ2) + response2 = await self._read_cc1101_register_by_address(FREQ2) freq2 = extract_hex_value(response2["register_value"]) # FREQ1 (0E) - response1 = await self.read_cc1101_register(FREQ1) + response1 = await self._read_cc1101_register_by_address(FREQ1) freq1 = extract_hex_value(response1["register_value"]) # FREQ0 (0F) - response0 = await self.read_cc1101_register(FREQ0) + response0 = await self._read_cc1101_register_by_address(FREQ0) freq0 = extract_hex_value(response0["register_value"]) # Die Register bilden eine 24-Bit-Zahl: (FREQ2 << 16) | (FREQ1 << 8) | FREQ0 @@ -524,6 +562,12 @@ def create_value_schema(value_schema: Dict[str, Any]) -> Dict[str, Any]: "description": "Frequency Deviation in kHz (float)." }) +CC1101_REGISTER_SCHEMA = create_value_schema({ + "type": "string", + "pattern": r"^[A-Z0-9]{4,8}$", # Registername wie IOCFG2, MCSM0, etc. + "description": "CC1101 register name (e.g., 'IOCFG2', 'MCSM0')." +}) + # --- SEND MSG SCHEMA (PHASE 2) --- SEND_MSG_SCHEMA = { "type": "object", @@ -556,7 +600,7 @@ def create_value_schema(value_schema: Dict[str, Any]) -> Dict[str, Any]: 'get/config/decoder': { 'method': 'get_config', 'schema': BASE_SCHEMA, 'description': 'Decoder configuration (CG)' }, 'get/cc1101/config': { 'method': 'get_ccconf', 'schema': BASE_SCHEMA, 'description': 'CC1101 configuration registers (C0DnF)' }, 'get/cc1101/patable': { 'method': 'get_ccpatable', 'schema': BASE_SCHEMA, 'description': 'CC1101 PA table (C3E)' }, - 'get/cc1101/register': { 'method': 'read_cc1101_register', 'schema': BASE_SCHEMA, 'description': 'Read CC1101 register (C)' }, + 'get/cc1101/register': { 'method': 'read_cc1101_register', 'schema': CC1101_REGISTER_SCHEMA, 'description': 'Read CC1101 register (C)' }, 'get/cc1101/frequency': { 'method': 'get_frequency', 'schema': BASE_SCHEMA, 'description': 'CC1101 current RF frequency' }, 'get/cc1101/settings': { 'method': 'get_cc1101_settings', 'schema': BASE_SCHEMA, 'description': 'CC1101 key configuration settings (freq, bw, rampl, sens, dr)' }, diff --git a/signalduino/controller.py b/signalduino/controller.py index a4791e1..552a7a2 100644 --- a/signalduino/controller.py +++ b/signalduino/controller.py @@ -166,6 +166,10 @@ async def get_cc1101_settings(self, payload: Dict[str, Any]) -> Dict[str, Any]: """Delegates to SignalduinoCommands to get all key CC1101 settings.""" return await self.commands.get_cc1101_settings(payload) + async def read_cc1101_register(self, payload: Dict[str, Any]) -> Dict[str, Any]: + """Reads a specific CC1101 register value by name (e.g., 'IOCFG2').""" + return await self.commands.read_cc1101_register(payload, timeout=SDUINO_CMD_TIMEOUT) + async def send_command( self, command: str, diff --git a/tests/test_mqtt_commands.py b/tests/test_mqtt_commands.py index d807fe9..7accc23 100644 --- a/tests/test_mqtt_commands.py +++ b/tests/test_mqtt_commands.py @@ -5,6 +5,7 @@ from asyncio import Queue import re +import json import pytest from aiomqtt import Client as AsyncMqttClient @@ -556,3 +557,54 @@ async def test_controller_handles_get_cc1101_settings(signalduino_controller, mo rampl_mock.assert_called_once() sens_mock.assert_called_once() dr_mock.assert_called_once() + + +@pytest.mark.asyncio +async def test_controller_handles_get_cc1101_register(signalduino_controller, mock_aiomqtt_client_cls, mock_logger): + """ + Testet den 'get/cc1101/register' MQTT-Befehl. + Es wird erwartet, dass der Registername im Payload enthalten ist und die Antwort + die geparste Registerinformation zurückgibt. + """ + + # 1. Mock _read_cc1101_register_by_address, die die rohe Hardware-Antwort liefert. + # MDMCFG4 hat Adresse 0x10. Die erwartete Antwort ist C10 = . + raw_response_line = "C10 = 02" # Beispielwert + + # Die Commands-Methode, die wir mocken müssen, ist _read_cc1101_register_by_address, + # da die öffentliche Methode read_cc1101_register sie aufruft. + # Da wir uns außerhalb der Klasse befinden, ist dies kompliziert. Stattdessen mocken wir + # die gesamte read_cc1101_register Methode in commands.py. + + # Wir stellen den Mock für die öffentliche Methode in Commands.py bereit: + # `async def read_cc1101_register(self, register_name: str, ...)` + expected_result_data = { + "register_value": raw_response_line, + "register_name": "MDMCFG4", + "address_hex": "10" + } + + read_reg_mock = AsyncMock(return_value=expected_result_data) + signalduino_controller.commands.read_cc1101_register = read_reg_mock + + # 2. Dispatcher und Payload vorbereiten + register_name = "MDMCFG4" + command_path = "get/cc1101/register" + mqtt_payload = f'{{"req_id": "test_reg", "value": "{register_name}"}}' + + dispatcher = MqttCommandDispatcher(controller=signalduino_controller) + + async with signalduino_controller: + + # 3. Dispatch ausführen + result = await dispatcher.dispatch(command_path, mqtt_payload) + + # 4. Assertions + assert result['status'] == "OK" + assert result['req_id'] == "test_reg" + assert result['data'] == expected_result_data + + # 5. Verifiziere, dass die Commands-Methode mit dem korrekten Payload aufgerufen wurde + expected_payload_dict = json.loads(mqtt_payload) + read_reg_mock.assert_called_once_with(expected_payload_dict, timeout=SDUINO_CMD_TIMEOUT) +