Skip to content

Commit 188de18

Browse files
authored
Merge pull request #10 from RFD-FHEM/fix/mqtt_consitency
Fix mqtt consitency in responses
2 parents 56ca564 + 408039c commit 188de18

9 files changed

Lines changed: 636 additions & 170 deletions

File tree

.roo/mcp.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"mcpServers":{"filesystem":{"command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","/workspaces/PySignalduino"],"alwaysAllow":["edit_file","read_text_file","search_files","read_multiple_files"]},"git":{"command":"uvx","args":["mcp-server-git","--repository","/workspaces/PySignalduino"],"alwaysAllow":["git_diff_unstaged","git_checkout"]}}}
1+
{"mcpServers":{"filesystem":{"command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","/workspaces/PySignalduino"],"alwaysAllow":["edit_file","read_text_file","search_files","read_multiple_files","create_directory","list_directory","directory_tree"]},"git":{"command":"uvx","args":["mcp-server-git","--repository","/workspaces/PySignalduino"],"alwaysAllow":["git_diff_unstaged","git_checkout"]}}}

docs/01_user_guide/mqtt_api.adoc

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,47 @@ Eine erfolgreiche Response auf `signalduino/v1/responses` hat folgende Struktur:
6565
}
6666
----
6767

68+
[[_cli_tool]]
69+
== CLI Tool zur Steuerung (`tools/sd_mqtt_cli.py`)
70+
71+
Das Skript `tools/sd_mqtt_cli.py` dient als einfaches Python-Kommandozeilen-Tool, um Befehle an das PySignalduino MQTT-Gateway zu senden und die Antworten zu empfangen.
72+
73+
=== Installation und Ausführung
74+
75+
Das Tool benötigt die `paho-mqtt` Abhängigkeit, die in der `requirements-dev.txt` enthalten ist.
76+
77+
[source,bash]
78+
----
79+
pip install paho-mqtt
80+
python3 tools/sd_mqtt_cli.py --help
81+
----
82+
83+
=== Verfügbare Kommandos
84+
85+
|===
86+
| Kommando | Beschreibung | Beispiel
87+
88+
| `reset`
89+
| Führt einen Factory Reset durch (`set/factory_reset`).
90+
| `python3 tools/sd_mqtt_cli.py reset`
91+
92+
| `get all-settings`
93+
| Fragt alle wichtigen CC1101-Einstellungen in einer aggregierten Nachricht ab.
94+
| `python3 tools/sd_mqtt_cli.py get all-settings`
95+
96+
| `get hardware-status --parameter <param>`
97+
| Fragt einen spezifischen CC1101-Parameter ab. Parameter: `frequency`, `bandwidth`, `rampl`, `sensitivity`, `datarate`.
98+
| `python3 tools/sd_mqtt_cli.py get hardware-status --parameter frequency`
99+
100+
| `get system-status --parameter <param>`
101+
| **NEU:** Fragt einen spezifischen System-Parameter ab. Parameter: `version`, `freeram`, `uptime`.
102+
| `python3 tools/sd_mqtt_cli.py get system-status --parameter freeram`
103+
104+
| `poll`
105+
| **NEU:** Fragt nacheinander alle verfügbaren System- und CC1101-Parameter ab. Nützlich zur Diagnose des aktuellen Gerätezustands.
106+
| `python3 tools/sd_mqtt_cli.py poll`
107+
|===
108+
68109
[[_get_commands]]
69110
== GET Commands (Status und Konfiguration abrufen)
70111

@@ -79,51 +120,51 @@ GET-Befehle benötigen eine leere Payload (`{}`) oder nur eine `req_id`.
79120
| Firmware-Version.
80121

81122
| `get/system/freeram`
82-
| `"1234"`
83-
| Verfügbarer RAM-Speicher.
123+
| `1234`
124+
| Verfügbarer RAM-Speicher (`int`).
84125

85126
| `get/system/uptime`
86-
| `"56789"`
87-
| System-Laufzeit.
127+
| `56789`
128+
| System-Laufzeit (`int`).
88129

89130
| `get/config/decoder`
90-
| `"MS=1;MU=1;MC=1;MN=1"`
91-
| Aktuelle Decoder-Konfiguration (aktivierte Protokollfamilien).
131+
| `{"MS": 1, "MU": 1, "MC": 1, "MN": 1}`
132+
| Aktuelle Decoder-Konfiguration (aktivierte Protokollfamilien) als geparstes Dictionary.
92133

93134
| `get/cc1101/config`
94-
| `"C0D11=0F"`
95-
| CC1101 Konfigurationsregister-Dump.
135+
| `{"cc1101_config_string": "C0D11=0F"}`
136+
| CC1101 Konfigurationsregister-Dump als gekapselter String.
96137

97138
| `get/cc1101/patable`
98-
| `"C3E = C0 C1 C2 C3 C4 C5 C6 C7"`
139+
| `{"pa_table_hex": "C3E = C0 C1 C2 C3 C4 C5 C6 C7"}`
99140
| CC1101 PA-Tabelle.
100141

101142
| `get/cc1101/register`
102-
| `"C00 = 29"`
143+
| `{"register_value": "C00 = 29"}`
103144
| Liest den Wert eines einzelnen CC1101-Registers (Adresse 0x00). Der Befehl nimmt keinen Wert in der Payload entgegen und liest standardmäßig Register 0x00.
104145

105146
| `get/cc1101/frequency`
106-
| `{"frequency_mhz": 868.3500}`
147+
| `{"frequency": 868.3500}`
107148
| Aktuelle RF-Frequenz in MHz.
108149

109150
| `get/cc1101/bandwidth`
110-
| `102.0`
151+
| `{"bandwidth": 102.0}`
111152
| Aktuelle IF-Bandbreite in kHz.
112153

113154
| `get/cc1101/rampl`
114-
| `30`
155+
| `{"rampl": 30}`
115156
| Aktuelle Empfängerverstärkung (LNA Gain) in dB. Mögliche Werte: `24, 27, 30, 33, 36, 38, 40, 42`.
116157

117158
| `get/cc1101/sensitivity`
118-
| `12`
159+
| `{"sensitivity": 12}`
119160
| Aktuelle Empfindlichkeit in dB. Mögliche Werte: `4, 8, 12, 16`.
120161

121162
| `get/cc1101/datarate`
122-
| `4.8`
163+
| `{"datarate": 4.8}`
123164
| Aktuelle Datenrate in kBaud.
124165

125166
| `get/cc1101/settings`
126-
| `{"frequency_mhz": 868.35, "bandwidth": 102.0, "rampl": 30, "sens": 12, "datarate": 4.8}`
167+
| `{"frequency": 868.35, "bandwidth": 102.0, "rampl": 30, "sensitivity": 12, "datarate": 4.8}`
127168
| Aggregierte Abfrage aller CC1101-Haupteinstellungen.
128169
|===
129170

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
= ADR-004: Strukturiertes Parsing serieller Antworten für MQTT GET-Befehle
2+
:revdate: 2026-01-06
3+
:author: Roo
4+
5+
== 1. Kontext
6+
7+
Die MQTT-Befehle `get/cc1101/*` (z.B. `get/cc1101/config`) und `get/config/decoder` schlagen mit Timeouts fehl, obwohl die serielle Kommunikation mit der SIGNALDuino-Firmware die Antworten empfängt. Die Ursache liegt darin, dass der `MqttCommandDispatcher` eine strukturierte JSON-Payload (ein Python-Dictionary) als `data`-Feld in der MQTT-Antwort erwartet. Die zugrundeliegenden `SignalduinoCommands` Methoden geben jedoch in diesen Fällen den *rohen* String der seriellen Firmware-Antwort zurück.
8+
9+
Der `MqttCommandDispatcher` kann diese String-Antworten nicht direkt in das JSON-Antwortformat umwandeln, was zu einem Abbruch der Verarbeitung und damit zum Timeout führt.
10+
11+
Betroffene Befehle und ihre Rohantwortformate:
12+
* `get/config/decoder` (CG): `MS=1;MU=1;MC=1;Mred=1\n`
13+
* `get/cc1101/config` (C0DnF): `C0Dn11=<Hex-Wert>\n`
14+
15+
Zusätzlich müssen alle `get` Befehle, die einen rohen String zurückgeben, angepasst werden, um die Konsistenz des MQTT-API zu gewährleisten.
16+
17+
== 2. Entscheidung
18+
19+
Wir werden die `SignalduinoCommands` Methoden, die serielle GET-Befehle ausführen, so modifizieren, dass sie die rohe Firmware-Antwort parsen und ein konsistentes Python-Dictionary (`Dict[str, Any]`) zurückgeben. Dieses Dictionary wird dann vom `MqttCommandDispatcher` als JSON-Payload im `data`-Feld der MQTT-Antwort verwendet.
20+
21+
Dies stellt sicher, dass alle erfolgreichen `GET` Anfragen über MQTT eine strukturierte und maschinenlesbare JSON-Antwort erhalten und die Timeouts vermieden werden.
22+
23+
=== Detaillierte Logik-Anpassungen
24+
25+
1. **`get_config` (CG):**
26+
* Wird eine private Hilfsfunktion `_parse_decoder_config(response: str) -> Dict[str, int]` in [`signalduino/commands.py`](signalduino/commands.py) implementiert.
27+
* Diese Funktion parst den `key=value;` String in ein Dictionary (z.B. `{'MS': 1, 'MU': 1, 'MC': 1, 'Mred': 1}`).
28+
* Der Rückgabetyp von `get_config` wird von `str` auf `Dict[str, int]` geändert.
29+
30+
2. **`get_ccconf` (C0DnF):**
31+
* Diese Methode gibt einen String wie `C0Dn11=<Hex-Wert>` zurück.
32+
* Die Methode wird angepasst, um die rohe String-Antwort in ein Dictionary zu kapseln, z.B. `{'cc1101_config_string': response_string}`.
33+
* Der Rückgabetyp von `get_ccconf` wird von `str` auf `Dict[str, str]` geändert.
34+
35+
3. **Weitere einfache GET-Befehle:**
36+
* Methoden wie `get_version`, `get_free_ram`, `get_uptime` geben bereits einen geparsten Wert zurück (String oder Int), der korrekt gekapselt wird. Diese Methoden bleiben unverändert, da sie bereits einen strukturierten Wert zurückgeben, der indirekt im `data`-Feld des MQTT-Payloads landet.
37+
38+
== 3. Konsequenzen
39+
40+
=== Positive
41+
* **Behebung der Timeouts:** Die MQTT GET-Befehle für Konfigurationen werden korrekt beantwortet und die Timeouts behoben.
42+
* **API-Konsistenz:** Alle MQTT `GET` Antworten liefern nun eine konsistente, JSON-serialisierbare Struktur.
43+
* **Wartbarkeit:** Der Code wird robuster, da das Parsing der seriellen Antwort in der `commands.py`-Schicht zentralisiert ist.
44+
45+
=== Negative
46+
* **Refactoring:** Es müssen kleinere Refactorings in [`signalduino/commands.py`](signalduino/commands.py) durchgeführt werden, um die Rückgabetypen der Methoden anzupassen.
47+
* **Tests/Dokumentation:** Die zugehörigen Unittests in [`tests/test_mqtt_commands.py`](tests/test_mqtt_commands.py) und die MQTT API Dokumentation in [`docs/01_user_guide/mqtt_api.adoc`](docs/01_user_guide/mqtt_api.adoc) müssen aktualisiert werden.
48+
49+
== 4. Alternativen
50+
51+
1. **Alternative 1: Parsing im `MqttCommandDispatcher`:** Die Rohergebnisse als `str` beibehalten und das Parsen spezifischer Befehlsantworten direkt im `MqttCommandDispatcher` durchführen.
52+
* *Nachteil:* Vermischt die Zuständigkeiten. Der Dispatcher sollte nur das Routing und die Validierung übernehmen, während die `SignalduinoCommands` die Logik für die Kommunikation und das Parsen der Firmware-spezifischen Antworten enthalten sollten.
53+
* *Abgelehnt* wegen schlechter Architektur und Verstoß gegen das Single Responsibility Principle.
54+
55+
2. **Alternative 2: Globaler, einfacher String-Wrapper im Dispatcher:** Jede String-Antwort global in ein einfaches Dictionary wie `{'response': <String>}` verpacken.
56+
* *Nachteil:* Führt zu einer inkonsistenten API, da einige Befehle (wie `get/config/decoder`) semantisch reiche, parsbare Daten liefern, die als roher String versteckt wären.
57+
* *Abgelehnt* zugunsten einer semantisch korrekten, strukturierten Antwort.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
= ADR 005: Vereinheitlichung der MQTT-Antwortstruktur für CC1101-Parameter
2+
:doctype: article :encoding: utf-8 :lang: de :status: Proposed :decided-at: 2026-01-07 :decided-by: Roo
3+
:toc: left
4+
5+
[[kontext]]
6+
== Kontext
7+
8+
Aktuell weichen die JSON-Antwortstrukturen für die Abfrage einzelner CC1101-Parameter via MQTT (z.B. Topic `get/cc1101/bandwidth`) von der Struktur der Gesamt-Abfrage (Topic `get/cc1101/settings`) ab.
9+
10+
* **Aktuelle Einzelabfrage (angenommen):** `get/cc1101/bandwidth` -> `{"bandwidth": "X kHz"}`
11+
* **Aktuelle Gesamtabfrage (angenommen):** `get/cc1101/settings` -> `{"cc1101": {"bandwidth": "X kHz", "rampl": "Y dbm", ...}}`
12+
13+
Diese Inkonsistenz erschwert die automatisierte Verarbeitung der Antworten, da Clients je nach Abfragetyp unterschiedliche JSON-Pfade parsen müssen. Ziel ist eine konsistente Struktur, bei der die JSON-Knotennamen für die einzelnen Parameter in beiden Abfragetypen identisch sind.
14+
15+
[[entscheidung]]
16+
== Entscheidung
17+
18+
Die JSON-Antwortstruktur für alle CC1101-Parameter-Abfragen wird vereinheitlicht. Die Schlüsselnamen der einzelnen Parameter in der JSON-Antwort werden in beiden Abfragetypen (Einzelparameter und Gesamt-Settings) identisch verwendet. Es wird entschieden, die Schlüssel der Einzelparameter ohne umschließendes Wrapper-Objekt zu verwenden.
19+
20+
* **Antwort auf `get/cc1101/parameter` (z.B. `get/cc1101/bandwidth`):**
21+
```json
22+
{"bandwidth": "X kHz"}
23+
```
24+
* **Antwort auf `get/cc1101/settings`:**
25+
```json
26+
{
27+
"bandwidth": "X kHz",
28+
"rampl": "Y dbm",
29+
"sensitivity": "Z",
30+
"datarate": "A kbps"
31+
}
32+
```
33+
Die `settings`-Antwort ist somit eine direkte Aggregation der Einzelparameter-Antworten.
34+
35+
[[konsequenzen]]
36+
== Konsequenzen
37+
38+
=== Positive Konsequenzen
39+
* **Konsistenz:** Vereinfacht das Parsen für MQTT-Clients, da die logischen Parameternamen (z.B. `bandwidth`) immer als JSON-Schlüssel auf der obersten Ebene der jeweiligen Antwort verwendet werden.
40+
* **Wartbarkeit:** Reduziert die Komplexität in der Implementierung, da die Logik zur Generierung der Parameterdaten wiederverwendet werden kann.
41+
42+
=== Negative Konsequenzen
43+
* **Breaking Change:** Bestehende Clients, die sich auf eine Wrapper-Struktur wie `{"cc1101": {...}}` bei der Gesamt-Abfrage (`get/cc1101/settings`) verlassen, müssen angepasst werden.
44+
* **Migration:** Die Server-Logik für die MQTT-Antworten in der PySignalduino-Implementierung muss entsprechend geändert werden.
45+
46+
[[alternativen]]
47+
== Alternativen
48+
* **Alternative A: Wrapper in Einzelabfragen beibehalten:** Man könnte die Einzelabfrage um den CC1101-Wrapper erweitern (z.B. `get/cc1101/bandwidth` -> `{"cc1101": {"bandwidth": "X kHz"}}`). Dies wurde abgelehnt, da es unnötige Verschachtelung für Einzelwerte einführt und die Lesbarkeit des Payloads verschlechtert.
49+
* **Alternative B: Einzelabfragen als reiner Wert:** Die Antwort könnte nur den reinen Wert zurückgeben (z.B. `get/cc1101/bandwidth` -> `"X kHz"`). Dies wurde abgelehnt, da es das JSON-Format verlässt und der Parametername im Payload verloren ginge, was die Eindeutigkeit erschwert.

0 commit comments

Comments
 (0)