Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,33 @@ This file provides guidance to agents when working with code in this repository.
oder um eine längere Laufzeit zu analysieren:
`python3 main.py --timeout 30`

## Testausführung im Devcontainer
- **Tests laufen im Devcontainer des Projekts, nicht in einer selbst angelegten Umgebung.** Im
Repository-Wurzelverzeichnis existiert bewusst kein Virtualenv; die Abhängigkeiten und die
Begleitdienste (Mosquitto, FHEM) sind in `.devcontainer/` definiert. Eine ad-hoc erzeugte
Umgebung läuft daran vorbei, kann abweichende Paketversionen ziehen und liefert Messergebnisse,
die nicht die des Projekts sind.
- In VS Code genügt "Reopen in Container"; der `postCreateCommand` aus
`.devcontainer/devcontainer.json` installiert `requirements-dev.txt` und `requirements.txt`.
- Ohne VS Code lässt sich derselbe Service direkt über Compose nutzen:
```bash
cd .devcontainer
docker compose up -d devcontainer
docker compose exec -T devcontainer bash -lc \
"cd /workspaces/PySignalduino && pip3 install --quiet --user -r requirements-dev.txt -r requirements.txt"
docker compose exec -T devcontainer bash -lc \
"cd /workspaces/PySignalduino && timeout 120 python3 -m pytest -q"
```
Hinweis: Über Compose gestartet greifen die `features` aus `devcontainer.json` (Node, uv,
AsciiDoc) nicht, und der `postCreateCommand` läuft nicht automatisch — deshalb der explizite
`pip3 install`. Für die Testsuite reicht das aus.
- Der FHEM-Dienst desselben Compose-Setups schreibt nach `.devcontainer/fhem-data/`. Das Image legt
diese Dateien standardmäßig unter UID/GID 6061 an, wodurch Git sie auf dem Host nicht mehr
ersetzen kann — ein Branch-Wechsel scheitert dann mit `unable to unlink old '<datei>':
Permission denied`. Abhilfe schafft, im `fhem`-Service die Variablen `FHEM_UID` und `FHEM_GID`
auf den eigenen Host-Benutzer zu setzen (`id -u` / `id -g`); der Dateibesitz sollte dagegen
nicht per `chown` umgebogen werden, da FHEM im Container sonst nicht mehr schreiben kann.

## Test Timeout Configuration
- Für pytest wurde ein globaler Timeout von 30 Sekunden in der `pyproject.toml` konfiguriert:
```toml
Expand Down
2 changes: 1 addition & 1 deletion README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Die SIGNALDuino-Firmware (Microcontroller-Code) wird in einem separaten Reposito
* **Vollständig asynchron** – Basierend auf `asyncio` für hohe Performance und einfache Integration in asynchrone Anwendungen.
* **MQTT-Integration** – Automatisches Publizieren dekodierter Nachrichten in konfigurierbare Topics und Empfang von Steuerbefehlen (z.B. `version`, `set`, `mqtt`).
* **Unterstützte Transporte** – Serielle Verbindung (über `pyserial-asyncio`) und TCP-Verbindung.
* **Umfangreiche Protokollbibliothek** – Portierung der originalen FHEM‑SIGNALDuino‑Protokolle mit `SDProtocols` und `SDProtocolData`.
* **Umfangreiche Protokollbibliothek** – Portierung der originalen FHEM‑SIGNALDuino‑Protokolle über `SDProtocols`; die Protokolldefinitionen liegen in `sd_protocols/protocols.json`.
* **Konfiguration über Umgebungsvariablen** – Einfache Einrichtung ohne Codeänderungen.
* **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.
Expand Down
2 changes: 1 addition & 1 deletion docs/01_user_guide/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ Die Hauptkomponenten sind:

3. **Protokollbibliothek** (`sd_protocols`):
* `SDProtocols` – Hauptklasse für Protokollerkennung und ‑dekodierung.
* `SDProtocolData` – Datenstrukturen für Protokolldefinitionen.
* `protocols.json` – Protokolldefinitionen, erzeugt aus den FHEM‑Perl‑Quellen via `tools/convert.pl`.

4. **Controller** (`signalduino.controller`):
* `SignalduinoController` – Zentrale Steuerungsklasse, koordiniert Transport, Parser und MQTT.
Expand Down
135 changes: 135 additions & 0 deletions docs/02_developer_guide/decoder_specs.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
= Decoder-Spezifikationen (Stufe 2)
:sectlinks:

Der Empfang läuft in zwei Stufen, wie in link:../architecture/decisions/ADR-006-sensor-decoding-layer.adoc[ADR-006] beschrieben:

. *Stufe 1* — `signalduino/parser` und `sd_protocols` machen aus Pulsen eine Bitfolge und daraus einen Hex-Payload.
. *Stufe 2* — `signalduino/decoders` macht daraus Messwerte: Temperatur, Luftfeuchte, Batteriezustand, Sensor-ID, Kanal.

Dieses Kapitel beschreibt, wie man Stufe 2 um ein Protokoll erweitert.

== Der Regelfall: eine JSON-Datei

Die meisten Protokolle lassen sich vollständig als Daten beschreiben. Eine Spezifikation liegt unter `signalduino/decoders/specs/` und wird beim Start gegen `spec_schema.json` validiert; ein Tippfehler fällt damit sofort beim Laden auf und nicht still bei jedem Telegramm.

[source,json]
----
{
"protocol_id": "900",
"model": "TEST_900",
"sensor_type": "Beispielgerät",
"prematch": "^12",
"crc": {
"algorithm": "crc8",
"data": { "from": 0, "to": 5 },
"check": { "from": 6, "to": 7 }
},
"fields": {
"id": { "source": "hex", "from": 0, "to": 1, "type": "str" },
"channel": { "source": "bits", "from": 17, "to": 19, "offset": 1 },
"temperature": { "source": "bits", "from": 22, "to": 31,
"type": "float", "offset": -400, "scale": 0.1, "unit": "°C" }
},
"limits": { "temperature": [-40, 60] }
}
----

Die Namen unter `fields` sind bewusst die FHEM-Readingnamen (`temperature`, `humidity`, `batteryState`, `channel`, ...). Sie sind über rund 150 Protokolle hinweg etabliert, machen den Paritätstest gegen die FHEM-Testvektoren zu einem direkten Vergleich und werden von den Ausgabeadaptern auf deren jeweilige Namen abgebildet.

== Indizes

`from` und `to` sind **nullbasiert und einschließlich** — `from: 18, to: 27` ist ein Zehn-Bit-Feld. Das entspricht `SD_WS_binaryToNumber($bitData, 18, 27)` in FHEM, sodass sich die Bitpositionen aus den Kommentaren der Perl-Module direkt übernehmen lassen.

`source` wählt die Datenquelle:

* `bits` — die demodulierte Bitfolge aus `metadata["bits"]`. Fehlt sie, etwa bei MN-Telegrammen, wird sie aus dem Hex-Payload erzeugt.
* `hex` — die Zeichen des Hex-Payloads, ohne Präambel.

== Rechenschritte eines Feldes

Die Reihenfolge liegt fest und ist so gewählt, dass eine Regel sich liest wie die FHEM-Zeile, die sie ersetzt:

. Bereich ausschneiden
. `bcd` — als binär codierte Dezimalzahl lesen
. `sign_bit` mit `sign_style` anwenden
. `offset` addieren
. `scale` multiplizieren
. `round` auf Nachkommastellen kürzen
. `map` — Rohwert über eine Wertetabelle abbilden
. `derive` — abgeleiteten Wert berechnen

[cols="1,3", options="header"]
|===
| Schlüssel | Bedeutung

| `type`
| `int` (Vorgabe), `float`, `str`, `bool`. `str` liefert den Rohausschnitt, solange die Regel keine Rechenoperation enthält — typisch für Sensor-IDs.

| `sign_style`
| `offset` zieht `sign_offset` ab (das FHEM-Idiom `wert - 1024`), `negate` kehrt das Vorzeichen um, `twos_complement` liest den Bereich im Zweierkomplement.

| `sign_value`
| Welcher Wert des Vorzeichenbits negativ bedeutet, Vorgabe `"1"`.

| `map`
| Bildet den Rohwert ab, z. B. `{"0": "ok", "1": "low"}` für den Batteriezustand.

| `derive`
| Abgeleiteter Wert, derzeit `wind_dir_text`. Mit `derive_from` rechnet die Regel auf einem bereits dekodierten Feld statt auf einem eigenen Bereich.

| `unit`
| Einheit, die im `SensorEvent` neben dem Wert geführt wird.
|===

== Prüfsummen

`crc` verweist auf einen Algorithmus aus `signalduino/decoders/crc.py` und auf zwei Bereiche des Hex-Payloads: `data` ist der geprüfte Bereich, `check` die im Telegramm mitgesendete Prüfsumme. Beide Bereiche sind Zeichenindizes und müssen ganze Bytes umfassen.

Verfügbar sind `crc8`, `crc16`, `crc16lsb`, `sum8`, `xor8`, `lfsr_digest8` und `lfsr_digest8_reflect`. Abweichende Parameter stehen unter `params`, etwa `{"poly": 49, "init": 255}`.

Schlägt die Prüfung fehl, wird das Telegramm verworfen — es entsteht kein `SensorEvent`. Genauso wirkt eine Verletzung von `limits`; das entspricht dem Verhalten der FHEM-Module, die bei unplausiblen Werten ebenfalls nichts liefern.

== Varianten

Trägt ein Protokoll je nach Telegrammtyp unterschiedliche Felder, wählt `variant_selector` den passenden Block aus `variants` aus. Die Felder unter `fields` gelten für alle Varianten, der Variantenblock ergänzt und überschreibt sie; `model` und `sensor_type` lassen sich pro Variante anpassen.

[source,json]
----
{
"variant_selector": { "source": "hex", "from": 0, "to": 1, "type": "str" },
"fields": { "id": { "source": "hex", "from": 2, "to": 3, "type": "str" } },
"variants": {
"30": { "model": "SD_WS_125_TH", "fields": { "temperature": { "…": "…" } } },
"52": { "model": "SD_WS_125_T", "fields": { "…": "…" } }
}
}
----

== Der Ausnahmefall: ein Python-Decoder

Lässt sich ein Protokoll nicht als Daten beschreiben — etwa weil Felder voneinander abhängen, die Prüfsumme über einen umsortierten Payload läuft oder der Modellname selbst aus den Daten entsteht — gehört es nach `signalduino/decoders/custom/`:

[source,python]
----
from ..registry import register_decoder

@register_decoder("115")
def decode_bresser_5in1(message):
...
return SensorEvent(...)
----

Ein solcher Decoder hat Vorrang vor einer gleichnamigen Spezifikation. Damit lässt sich ein Protokoll später von der einen Form in die andere überführen, ohne etwas zu löschen.

== Routing

Gefunden wird ein Decoder über die `protocol_id`, die in `DecodedMessage` bereits vorliegt. Die Felder aus `protocols.json` dienen dabei nur der Vorbereitung: `preamble` trennt den Hex-Teil ab, `modulematch` ist der Vorgabewert für `prematch`, und `clientmodule` gruppiert die Protokolle für Topics und Berichte.

== Neues Protokoll hinzufügen

. Bitlayout und Prüfsumme im zugehörigen FHEM-Client-Modul nachlesen, etwa in `14_SD_WS.pm`.
. Spezifikation unter `signalduino/decoders/specs/` anlegen.
. Testvektoren vorhanden? `tests/data/fhem/` enthält die FHEM-Vektoren samt erwarteter Readings; ergänzt werden sie mit `tools/fhem_testdata_import.py`.
. Tests ausführen (siehe `AGENTS.md`, Abschnitt zur Testausführung im Devcontainer).

Stufe 2 kann Stufe 1 nicht beschädigen: Jeder Fehler im Decoder führt zu `sensor = None`, während der bisherige Nachrichtenfluss unverändert weiterläuft.
2 changes: 1 addition & 1 deletion docs/02_developer_guide/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

Dieser Abschnitt beschreibt die Architektur, wie man zur Entwicklung beitragen kann (Contributing) und wie man Tests durchführt.

include::architecture.adoc[] include::contribution.adoc[]
include::architecture.adoc[] include::contribution.adoc[] include::decoder_specs.adoc[]

== Weitere Ressourcen

Expand Down
Loading
Loading