Zweck und Funktion

Der SIGNALduino (bestehend aus Hardware-Stick und Firmware) ist ein leistungsstarkes I/O-Gerät zur Erfassung, zum Empfang und zur Verarbeitung von digitalen Funksignalen (typischerweise 433 MHz und 868 MHz).

Seine Hauptaufgabe ist es, Funksignale anhand von Mustern zu erkennen und sie in maximal detaillierter Form an die übergeordnete Hausautomations-Software (wie FHEM) zur Dekodierung weiterzugeben. Dadurch werden verschiedenste proprietäre Funkprotokolle für die Nutzung in Smart-Home-Systemen zugänglich.

Die verfügbare Hardware-Basis reicht von einfachen Arduino/nanoCUL-Lösungen bis hin zu erweiterten Varianten wie dem Maple-SignalDuino und dem ESP32-SignalDuino, die erweiterte Funktionen (z.B. WLAN) bieten.

SIGNALduino Übersicht - Hardware und Funktion

Projektgeschichte und Entwicklung

Das SIGNALduino-Projekt entstand in der RFD-FHEM-Community als Open-Source-Initiative zur kostengünstigen Funkkommunikation im Smart-Home-Bereich.

Ursprung und Meilensteine

  • 2010er Jahre: Entwicklung erster Arduino-basierter Transceiver mit CC1101-Chips für 433/868 MHz.

  • Perl-Ära: Die Protokollimplementierung erfolgte zunächst als FHEM-Modul 00_SIGNALduino.pm in Perl.

  • Community-Wachstum: Eine aktive Entwickler- und Anwendergemeinschaft trieb die Erweiterung der unterstützten Protokolle voran.

Migration zu Python

  • PySignalduino: Mit dem Aufkommen moderner IoT-Architekturen wurde eine Python-Implementierung notwendig.

  • Asynchrone Verarbeitung: PySignalduino nutzt asyncio für effiziente, nicht-blockierende Verarbeitung.

  • MQTT-Integration: Eingebaute MQTT-Bridge für nahtlose Integration in moderne Smart-Home-Systeme.

Firmware-Entwicklung

Die SIGNALDuino-Firmware wird kontinuierlich im separaten Repository weiterentwickelt:

  • GitHub Repository: https://github.com/RFD-FHEM/SIGNALDuino

  • Aktuelle Version: v3.5.0 (Stand Dezember 2025)

  • Unterstützte Plattformen:

    • Arduino Nano mit CC1101

    • ESP32 (mit WiFi-Unterstützung)

    • STM32 (Maple Mini)

  • Build-System: PlatformIO und Arduino-IDE Projekte sind im Repository enthalten.

PySignalduino vs. Original

PySignalduino ist keine direkte Portierung, sondern eine Neuimplementierung mit folgenden Schwerpunkten: - Moderne Python-Praktiken: Typisierung, strukturierte Logging, Konfiguration über Umgebungsvariablen. - Erweiterte Transporte: Unterstützung für serielle und TCP-Verbindungen. - Testabdeckung: Umfangreiche Testsuite zur Gewährleistung der Codequalität.

Entwicklungsstatus

Warning

Entwicklungsstatus

PySignalduino befindet sich noch in aktiver Entwicklung und hat noch kein offizielles Release veröffentlicht. Die API kann sich zwischen Versionen ändern. Entwickler sollten bei der Verwendung Vorsicht walten lassen und auf mögliche Breaking Changes vorbereitet sein.

Unterstützte Protokolle

Eine breite Palette von Funkprotokollen wird unterstützt und ständig erweitert. Detaillierte Informationen zu den unterstützten Geräten und Protokollen finden Sie im Benutzerhandbuch.

Firmware

Die Firmware wird kontinuierlich weiterentwickelt und ist nicht auf jedem prinzipiell geeigneten Gerät lauffähig, da spezifische Anpassungen an die Hardware erforderlich sind.

Migration

PySignalduino wurde von einer Thread-basierten Architektur zu einer asynchronen asyncio-Architektur migriert. Falls Sie von einer Version vor 0.9.0 upgraden, lesen Sie die Migrationsleitfäden:

Installation

Note

PySignalduino ist noch in Entwicklung. Es gibt bisher keine stabile Version – nutzen Sie die Software mit entsprechender Vorsicht.

Voraussetzungen

  • Python 3.8 oder höher

  • pip (Python Package Installer)

  • Ein SIGNALDuino-Gerät mit serieller oder TCP-Verbindung

  • Optional: Ein MQTT-Broker (z.B. Mosquitto) für die MQTT-Integration

Abhängigkeiten

PySignalduino benötigt folgende Python-Pakete:

  • pyserial – Serielle Kommunikation

  • pyserial-asyncio – Asynchrone serielle Unterstützung

  • aiomqtt – Asynchroner MQTT-Client (ersetzt paho-mqtt in der asynchronen Version)

  • python-dotenv – Laden von Umgebungsvariablen aus .env-Dateien

  • requests – HTTP-Anfragen (für Firmware-Download)

Diese Abhängigkeiten werden automatisch installiert, wenn Sie das Paket mit pip install -e . installieren.

Installation via pip (empfohlen)

Die einfachste Methode ist die Installation aus dem geklonten Repository im Entwicklermodus:

git clone https://github.com/Ein-Einfaches-Beispiel/PySignalduino.git
cd PySignalduino
pip install -e .

Dadurch wird das Paket signalduino-mqtt in Ihrer Python-Umgebung installiert und alle Runtime-Abhängigkeiten werden erfüllt.

Alternative: Installation nur der Abhängigkeiten

Falls Sie das Paket nicht installieren, sondern nur die Abhängigkeiten nutzen möchten (z.B. für Skripte im Projektverzeichnis):

pip install -r requirements.txt

Die Datei requirements.txt enthält die gleichen Pakete wie oben aufgelistet.

Entwicklungsumgebung einrichten

Für Beiträge zum Projekt oder zum Ausführen der Tests installieren Sie zusätzlich die Entwicklungsabhängigkeiten:

pip install -r requirements-dev.txt

Dies installiert:

  • pytest – Testframework

  • pytest-mock – Mocking-Unterstützung

  • pytest-asyncio – Asynchrone Testunterstützung

  • pytest-cov – Coverage-Berichte

Verifikation der Installation

Überprüfen Sie, ob die Installation erfolgreich war, indem Sie die Hilfe des Hauptprogramms aufrufen:

python3 main.py --help

Sie sollten eine Ausgabe mit allen verfügbaren Kommandozeilenoptionen sehen.

Docker / DevContainer

Für eine konsistente Entwicklungsumgebung steht eine DevContainer-Konfiguration bereit. Öffnen Sie das Projekt in Visual Studio Code mit der Remote-Containers-Erweiterung, um automatisch alle Abhängigkeiten in einem isolierten Container zu installieren.

Details finden Sie in der [DevContainer-Dokumentation](devcontainer_env.md).

Nächste Schritte

Nach der Installation können Sie:

  1. Die [Schnellstart-Anleitung](../index.adoc#_schnellstart) befolgen.

  2. Die [Konfiguration über Umgebungsvariablen](../usage.adoc#_konfiguration) einrichten.

  3. Die [MQTT-Integration](../usage.adoc#_mqtt_integration) testen.

Verwendung und Konfiguration

Grundlegende Nutzung

Die Hauptklasse SDProtocols stellt die Schnittstelle zur Protokollverarbeitung bereit.

    def __init__(self):
        self._protocols = self._load_protocols()
        self._log_callback = None
        self.set_defaults()

    def _load_protocols(self) -> Dict[str, Any]:
        """Loads protocols from protocols.json."""
        json_path = Path(__file__).resolve().parent / "protocols.json"
        try:
            with open(json_path, "r", encoding="utf-8") as f:
                data = json.load(f)
            return data.get("protocols", {})
        except Exception as e:
            # Fallback or error logging if needed, though for now we raise
            # or return empty dict if file missing (should not happen in prod)
            print(f"Error loading protocols.json: {e}")
            return {}

    def protocol_exists(self, pid: str) -> bool:
        return pid in self._protocols

    def get_protocol_list(self) -> dict:
        return self._protocols

Integration

PySignalduino ist als Bibliothek konzipiert, die beispielsweise in MQTT-Bridges oder Home-Automation-Skripten verwendet werden kann. Sie übernimmt die Erkennung und Dekodierung der Rohdaten.

Logging

Für Debugging-Zwecke können Sie eine eigene Callback-Funktion registrieren:

    def register_log_callback(self, callback):
        """Register a callback function for logging."""
        if callable(callback):
            self._log_callback = callback

    def _logging(self, message: str, level: int = 3):
        """Log a message if a callback is registered."""
        if self._log_callback:
            self._log_callback(message, level)

MQTT Integration

PySignalduino bietet eine integrierte MQTT-Integration über die Klasse MqttPublisher. Diese ermöglicht das Veröffentlichen dekodierter Nachrichten an einen MQTT-Broker und das Empfangen von Befehlen über MQTT-Topics.

Einrichtung und Konfiguration

Die MQTT-Verbindung wird automatisch initialisiert, wenn die Umgebungsvariable MQTT_HOST gesetzt ist. Folgende Umgebungsvariablen können konfiguriert werden:

  • MQTT_HOST – Hostname oder IP-Adresse des MQTT-Brokers (Standard: localhost)

  • MQTT_PORT – Port des Brokers (Standard: 1883)

  • MQTT_TOPIC – Basis-Topic für alle Nachrichten (Standard: signalduino)

  • MQTT_USERNAME – Optionaler Benutzername für Authentifizierung

  • MQTT_PASSWORD – Optionales Passwort für Authentifizierung

  • MQTT_COMPRESSION_ENABLED – Boolescher Wert (0/1) zur Aktivierung der Payload-Kompression (Standard: 0)

Der MqttPublisher wird innerhalb des SignalduinoController verwendet und stellt eine asynchrone Context-Manager-Schnittstelle bereit:

    # Transport initialisieren
    transport = None
    if args.serial:
        logger.info(f"Initialisiere serielle Verbindung auf {args.serial} mit {args.baud} Baud...")
        transport = SerialTransport(port=args.serial, baudrate=args.baud)
    elif args.tcp:
        logger.info(f"Initialisiere TCP Verbindung zu {args.tcp}:{args.port}...")
        transport = TCPTransport(host=args.tcp, port=args.port)

    # Wenn weder --serial noch --tcp (oder deren ENV-Defaults) gesetzt sind
    if not transport:
        logger.error("Kein gültiger Transport konfiguriert. Bitte geben Sie --serial oder --tcp an oder setzen Sie SIGNALDUINO_SERIAL_PORT / SIGNALDUINO_TCP_HOST in der Umgebung.")
        sys.exit(1)

    # Controller initialisieren
    controller = SignalduinoController(
        transport=transport,
        message_callback=message_callback,
        logger=logger
    )
    
    # Starten
    try:
        logger.info("Verbinde zum Signalduino...")
        # NEU: Verwende async with Block
        async with controller:
            logger.info("Verbunden! Starte Initialisierung und Hauptschleife...")
            
            # Starte die Hauptschleife, warte auf deren Beendigung oder ein Timeout
            await controller.run(timeout=args.timeout)

MQTT-Topics

  • {topic}/messages – JSON‑kodierte dekodierte Nachrichten (DecodedMessage)

  • {topic}/commands/# – Topic für eingehende Befehle (Wildcard-Subscription)

  • {topic}/result/{command} – Antworten auf Befehle (z. B. signalduino/result/version)

  • {topic}/status – Heartbeat‑ und Statusmeldungen (optional)

Heartbeat-Funktionalität

Der Publisher sendet regelmäßig einen Heartbeat („online“) unter {topic}/status, solange die Verbindung besteht. Bei Verbindungsabbruch wird „offline“ gepublished.

Beispiel: Manuelle Nutzung des MqttPublisher

    publisher = MqttPublisher()
    
    async with publisher:
        await publisher.publish(mock_decoded_message)

Command Interface

PySignalduino stellt eine umfangreiche Befehls-API zur Steuerung des SIGNALDuino-Firmware-Geräts bereit. Die Klasse SignalduinoCommands kapselt alle verfügbaren seriellen Befehle und bietet eine asynchrone Schnittstelle.

Verfügbare Befehle

Die folgenden Befehle werden unterstützt (Auswahl):

  • Systembefehle:

  • get_version() – Firmware-Version abfragen (V)

  • get_help() – Hilfe anzeigen (?)

  • get_free_ram() – Freien RAM abfragen ®

  • get_uptime() – Uptime in Sekunden (t)

  • ping() – Ping-Gerät (P)

  • get_cc1101_status() – CC1101-Status (s)

  • disable_receiver() – Empfänger deaktivieren (XQ)

  • enable_receiver() – Empfänger aktivieren (XE)

  • factory_reset() – Werkseinstellungen wiederherstellen (e)

  • Konfigurationsbefehle:

  • get_config() – Konfiguration lesen (CG)

  • set_decoder_state(decoder, enabled) – Decoder aktivieren/deaktivieren (C<CMD><FLAG>)

  • set_manchester_min_bit_length(length) – MC Min Bit Length setzen (CSmcmbl=)

  • set_message_type_enabled(message_type, enabled) – Nachrichtentyp aktivieren/deaktivieren (C<FLAG><TYPE>)

  • get_ccconf() – CC1101-Konfiguration abfragen (C0DnF)

  • get_ccpatable() – CC1101 PA Table abfragen (C3E)

  • read_cc1101_register(register) – CC1101-Register lesen (C<reg>)

  • write_register(register, value) – EEPROM/CC1101-Register schreiben (W<reg><val>)

  • read_eeprom(address) – EEPROM-Byte lesen (r<addr>)

  • set_patable(value) – PA Table schreiben (x<val>)

  • set_bwidth(value) – Bandbreite setzen (C10<val>)

  • set_rampl(value) – Rampenlänge setzen (W1D<val>)

  • set_sens(value) – Empfindlichkeit setzen (W1F<val>)

  • Sendebefehle:

  • send_combined(params) – Kombinierten Sendebefehl (SC…​)

  • send_manchester(params) – Manchester senden (SM…​)

  • send_raw(params) – Rohdaten senden (SR…​)

  • send_xfsk(params) – xFSK senden (SN…​)

  • send_message(message) – Vorkodierte Nachricht senden

Persistenz-Funktionalität

Befehle, die die Hardware-Konfiguration ändern (z. B. write_register, set_patable), werden in der Regel im EEPROM des SIGNALDuino persistent gespeichert. Die Persistenz wird durch die Firmware gewährleistet; PySignalduino sendet lediglich die entsprechenden Kommandos.

Nutzung über MQTT

Wenn MQTT aktiviert ist, können Befehle über das Topic signalduino/commands/{command} gesendet werden. Die Antwort erscheint unter signalduino/result/{command}.

Beispiel mit mosquitto_pub:

# Firmware-Version abfragen
mosquitto_pub -t "signalduino/commands/version" -m "GET"

# Empfänger aktivieren
mosquitto_pub -t "signalduino/commands/set/XE" -m "1"

Code-Beispiel: Direkte Nutzung der Command-API

    controller = SignalduinoController(transport=mock_transport, parser=mock_parser)
    async with controller:
        await start_controller_tasks(controller)
        
        # get_version uses send_command, which uses controller.commands._send, which calls controller.send_command
        # This will block until the response is received
        response = await controller.commands.get_version(timeout=1)
        
        mock_transport.write_line.assert_called_once_with("V")
        assert response is not None
        assert "SIGNALduino" in response

Beispiel: Asynchrone Context-Manager Nutzung

    # Transport initialisieren
    transport = None
    if args.serial:
        logger.info(f"Initialisiere serielle Verbindung auf {args.serial} mit {args.baud} Baud...")
        transport = SerialTransport(port=args.serial, baudrate=args.baud)
    elif args.tcp:
        logger.info(f"Initialisiere TCP Verbindung zu {args.tcp}:{args.port}...")
        transport = TCPTransport(host=args.tcp, port=args.port)

    # Wenn weder --serial noch --tcp (oder deren ENV-Defaults) gesetzt sind
    if not transport:
        logger.error("Kein gültiger Transport konfiguriert. Bitte geben Sie --serial oder --tcp an oder setzen Sie SIGNALDUINO_SERIAL_PORT / SIGNALDUINO_TCP_HOST in der Umgebung.")
        sys.exit(1)

    # Controller initialisieren
    controller = SignalduinoController(
        transport=transport,
        message_callback=message_callback,
        logger=logger
    )
    
    # Starten
    try:
        logger.info("Verbinde zum Signalduino...")
        # NEU: Verwende async with Block
        async with controller:
            logger.info("Verbunden! Starte Initialisierung und Hauptschleife...")
            
            # Starte die Hauptschleife, warte auf deren Beendigung oder ein Timeout
            await controller.run(timeout=args.timeout)

API-Referenz (Auszug)

Die folgenden Klassen und Schnittstellen sind für die Integration besonders relevant:

MqttPublisher

Die Klasse signalduino.mqtt.MqttPublisher bietet eine asynchrone Context-Manager-Schnittstelle zur Kommunikation mit einem MQTT-Broker.

  • Methoden:

  • async publish(message: DecodedMessage) – Veröffentlicht eine dekodierte Nachricht unter {topic}/messages

  • async publish_simple(subtopic: str, payload: str, retain: bool = False) – Veröffentlicht eine einfache Zeichenkette unter {topic}/{subtopic}

  • async is_connected() → bool – Prüft, ob die Verbindung zum Broker besteht

  • register_command_callback(callback: Callable) – Registriert einen asynchronen Callback für eingehende Befehle

  • Context-Manager: async with MqttPublisher() as publisher:

SignalduinoCommands

Die Klasse signalduino.commands.SignalduinoCommands kapselt alle seriellen Befehle für die SIGNALDuino-Firmware.

  • Initialisierung: Erfordert eine asynchrone Sendefunktion (wird normalerweise vom SignalduinoController bereitgestellt)

  • Alle Methoden sind asynchron (async def) und geben entweder str (Antwort) zurück oder None (keine Antwort erwartet)

  • Umfang: Systembefehle, Konfiguration, Senden von Nachrichten (siehe Abschnitt „Command Interface“)

Asynchrone Context-Manager-Schnittstelle

Sowohl SignalduinoController als auch MqttPublisher und die Transportklassen (TcpTransport, SerialTransport) implementieren das asynchrone Context-Manager-Protokoll (aenter/aexit). Dies gewährleistet eine sichere Ressourcenverwaltung (Verbindungsauf‑/abbau, Hintergrundtasks).

Beispiel für verschachtelte Context-Manager:

    # Transport initialisieren
    transport = None
    if args.serial:
        logger.info(f"Initialisiere serielle Verbindung auf {args.serial} mit {args.baud} Baud...")
        transport = SerialTransport(port=args.serial, baudrate=args.baud)
    elif args.tcp:
        logger.info(f"Initialisiere TCP Verbindung zu {args.tcp}:{args.port}...")
        transport = TCPTransport(host=args.tcp, port=args.port)

    # Wenn weder --serial noch --tcp (oder deren ENV-Defaults) gesetzt sind
    if not transport:
        logger.error("Kein gültiger Transport konfiguriert. Bitte geben Sie --serial oder --tcp an oder setzen Sie SIGNALDUINO_SERIAL_PORT / SIGNALDUINO_TCP_HOST in der Umgebung.")
        sys.exit(1)

    # Controller initialisieren
    controller = SignalduinoController(
        transport=transport,
        message_callback=message_callback,
        logger=logger
    )
    
    # Starten
    try:
        logger.info("Verbinde zum Signalduino...")
        # NEU: Verwende async with Block
        async with controller:
            logger.info("Verbunden! Starte Initialisierung und Hauptschleife...")
            
            # Starte die Hauptschleife, warte auf deren Beendigung oder ein Timeout
            await controller.run(timeout=args.timeout)

Weitere Klassen

  • SignalduinoController – Zentrale Steuerungsklasse, koordiniert Transport, Parser, MQTT und Befehle

  • TcpTransport, SerialTransport – Asynchrone Transportimplementierungen für TCP bzw. serielle Verbindungen

  • DecodedMessage, RawFrame – Datentypen für dekodierte Nachrichten und Rohframes

Eine vollständige API-Dokumentation kann mit pydoc oder mittels Sphinx generiert werden.

Troubleshooting

Dieser Abschnitt beschreibt häufige Probleme und deren Lösungen.

MQTT-Verbindungsprobleme

  • Keine Verbindung zum Broker: Stellen Sie sicher, dass die Umgebungsvariablen MQTT_HOST und MQTT_PORT korrekt gesetzt sind. Der Broker muss erreichbar sein und keine Authentifizierung erfordern (oder Benutzername/Passwort müssen gesetzt sein).

  • Verbindung bricht ab: Überprüfen Sie die Netzwerkverbindung und Broker-Konfiguration. Der MQTT-Client (aiomqtt) versucht automatisch, die Verbindung wiederherzustellen. Falls die Verbindung dauerhaft abbricht, prüfen Sie Firewall-Einstellungen und Broker-Logs.

  • MQTT-Nachrichten werden nicht empfangen: Stellen Sie sicher, dass das Topic {topic}/commands/# abonniert ist. Der Command-Listener startet automatisch, wenn MQTT aktiviert ist. Überprüfen Sie die Log-Ausgabe auf Fehler.

Asyncio-spezifische Probleme

  • RuntimeError: no running event loop: Tritt auf, wenn asyncio-Funktionen außerhalb eines laufenden Event-Loops aufgerufen werden. Stellen Sie sicher, dass Ihr Code innerhalb einer asyncio-Coroutine läuft und asyncio.run() verwendet wird. Verwenden Sie async with für Context-Manager.

  • Tasks hängen oder werden nicht abgebrochen: Alle Hintergrundtasks sollten auf das _stop_event reagieren. Bei manuell erstellten Tasks müssen Sie asyncio.CancelledError abfangen und Ressourcen freigeben.

  • Deadlocks in Queues: Wenn eine Queue voll ist und kein Consumer mehr liest, kann await queue.put() blockieren. Stellen Sie sicher, dass die Consumer-Tasks laufen und die Queue nicht überfüllt wird. Verwenden Sie asyncio.wait_for mit Timeout.

Verbindungsprobleme zum SIGNALDuino-Gerät

  • Keine Antwort auf Befehle: Überprüfen Sie die serielle oder TCP-Verbindung. Stellen Sie sicher, dass das Gerät eingeschaltet ist und die korrekte Baudrate (115200) verwendet wird. Testen Sie mit einem Terminal-Programm, ob das Gerät auf V (Version) antwortet.

  • Timeout-Errors: Die Standard-Timeout für Befehle beträgt 2 Sekunden. Bei langsamen Verbindungen kann dies erhöht werden. Falls Timeouts trotzdem auftreten, könnte die Verbindung instabil sein.

  • Parser erkennt keine Protokolle: Überprüfen Sie, ob die Rohdaten im erwarteten Format ankommen (z.B. +MU;…​). Stellen Sie sicher, dass die Protokolldefinitionen (protocols.json) geladen werden und das Protokoll aktiviert ist.

Logging und Debugging

Aktivieren Sie Debug-Logging, um detaillierte Informationen zu erhalten:

    # Konfiguration des Loggings
    logging.basicConfig(
        level=level,
        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
        handlers=[
            logging.StreamHandler(sys.stdout)
        ]
    )
    # Setze den Level auch auf den Root-Logger, falls basicConfig ihn nicht korrekt gesetzt hat (z.B. bei wiederholtem Aufruf)
    logging.getLogger().setLevel(level)

Die Log-Ausgabe zeigt den Status von Transport, Parser und MQTT.

Bekannte Probleme und Workarounds

  • Entwicklungsstatus: Da PySignalduino noch in aktiver Entwicklung ist, können sich Verhalten und API zwischen Commits ändern. Bei unerwartetem Verhalten prüfen Sie bitte die aktuelle Codebasis und melden Sie Issues auf GitHub.

  • aiomqtt-Versionen: Verwenden Sie aiomqtt>=2.0.0. Ältere Versionen können Inkompatibilitäten aufweisen.

  • Windows und asyncio: Unter Windows kann es bei seriellen Verbindungen zu Problemen mit asyncio kommen. Verwenden Sie asyncio.ProactorEventLoop oder weichen Sie auf TCP-Transport aus.

  • Memory Leaks: Bei langem Betrieb können asyncio-Tasks Speicher verbrauchen. Stellen Sie sicher, dass abgeschlossene Tasks garbage-collected werden. Verwenden Sie asyncio.create_task mit Referenzen, um Tasks später abbrechen zu können.

Bei weiteren Problemen öffnen Sie bitte ein Issue auf GitHub mit den relevanten Logs und Konfigurationsdetails.

Architektur

Übersicht

PySignalduino ist modular aufgebaut und trennt die Protokolldefinitionen (JSON) strikt von der Verarbeitungslogik (Python). Seit der Migration zu asyncio (Version 0.9.0) folgt das System einer ereignisgesteuerten, asynchronen Architektur, die auf asyncio-Tasks und -Queues basiert. Dies ermöglicht eine effiziente Verarbeitung von Sensordaten, Kommandos und MQTT-Nachrichten ohne Blockierung.

Kernkomponenten

SDProtocols Klasse

Die Klasse SDProtocols (sd_protocols/sd_protocols.py) ist der zentrale Einstiegspunkt. Sie vereint Funktionalitäten durch Mehrfachvererbung von Mixins:

  • ProtocolHelpersMixin: Grundlegende Bit-Operationen.

  • ManchesterMixin: Spezifische Logik für Manchester-kodierte Signale (mcBit2* Methoden).

  • PostdemodulationMixin: Nachbearbeitung dekodierter Daten (postDemo_* Methoden).

  • RSLMixin: Handler für das RSL-Protokoll.

Protokolldefinition (JSON)

Die Datei sd_protocols/protocols.json enthält die statischen Definitionen. Jedes Protokoll besitzt eine ID und Eigenschaften wie:

  • format: Kodierung (z.B. manchester, twostate, pwm).

  • preamble: Erkennungsmuster.

  • method: Mapping auf die Python-Methode zur Dekodierung.

Parsing Chain (Manchester)

Der Ablauf bei Manchester-Signalen ist wie folgt: 1. Erkennung: Match anhand der Preamble/Muster. 2. Vorvalidierung: ManchesterMixin._demodulate_mc_data() prüft Länge und Taktung. 3. Dekodierung: Aufruf der spezifischen mcBit2*-Methode.

Hinweis: Einige Protokolle wie TFA (mcBit2TFA) oder Grothe (mcBit2Grothe) haben spezielle Anforderungen an die Längenprüfung oder Duplikatfilterung.

Asyncio-Architektur

PySignalduino verwendet asyncio für alle E/A-Operationen, um parallele Verarbeitung ohne Thread-Overhead zu ermöglichen. Die Architektur basiert auf drei Haupt-Tasks, die über asynchrone Queues kommunizieren:

  • Reader-Task: Liest kontinuierlich Zeilen vom Transport (Seriell/TCP) und legt sie in der _raw_message_queue ab.

  • Parser-Task: Entnimmt Rohzeilen aus der Queue, dekodiert sie über den SignalParser und veröffentlicht Ergebnisse via MQTT oder ruft den message_callback auf.

  • Writer-Task: Verarbeitet Kommandos aus der _write_queue, sendet sie an das Gerät und wartet bei Bedarf auf Antworten.

Zusätzlich gibt es spezielle Tasks für Initialisierung, Heartbeat und MQTT-Command-Listener.

Asynchrone Queues und Synchronisation

  • _raw_message_queue (asyncio.Queue[str]): Rohdaten vom Reader zum Parser.

  • _write_queue (asyncio.Queue[QueuedCommand]): Ausstehende Kommandos vom Controller zum Writer.

  • _pending_responses (List[PendingResponse]): Verwaltet erwartete Antworten mit asyncio.Event für jede.

  • _stop_event (asyncio.Event): Signalisiert allen Tasks, dass sie beenden sollen.

  • _init_complete_event (asyncio.Event): Wird gesetzt, sobald die Geräteinitialisierung erfolgreich abgeschlossen ist.

Asynchrone Kontextmanager

Alle Ressourcen (Transport, MQTT-Client) implementieren aenter/aexit und werden mittels async with verwaltet. Der SignalduinoController selbst ist ein Kontextmanager, der die Lebensdauer der Verbindung steuert.

MQTT-Integration

Die MQTT-Integration erfolgt über die Klasse MqttPublisher (signalduino/mqtt.py), die auf aiomqtt basiert und asynchrone Veröffentlichung und Abonnement unterstützt.

Verbindungsaufbau

Der MQTT-Client wird automatisch gestartet, wenn die Umgebungsvariable MQTT_HOST gesetzt ist. Im aenter des Controllers wird der Publisher mit dem Broker verbunden und ein Command-Listener-Task gestartet.

Topics und Nachrichtenformat

  • Sensordaten: {MQTT_TOPIC}/messages – JSON‑Serialisierte DecodedMessage-Objekte.

  • Kommandos: {MQTT_TOPIC}/commands/{command} – Ermöglicht die Steuerung des Signalduino via MQTT (z.B. version, freeram, rawmsg).

  • Status: {MQTT_TOPIC}/status/{alive,data,version} – Heartbeat- und Gerätestatus.

Command-Listener

Ein separater asynchroner Loop (_command_listener) lauscht auf Kommando‑Topics, ruft den registrierten Callback (im Controller _handle_mqtt_command) auf und führt die entsprechende Aktion aus. Die Antwort wird unter result/{command} oder error/{command} zurückveröffentlicht.

Komponentendiagramm (Übersicht)

+-------------------+      +-------------------+      +-------------------+
|   Transport       |      |   Controller      |      |   MQTT Publisher  |
|   (Serial/TCP)    |----->|   (asyncio Tasks) |----->|   (aiomqtt)       |
+-------------------+      +-------------------+      +-------------------+
         ^                          |                          |
         |                          v                          v
+-------------------+      +-------------------+      +-------------------+
|   SIGNALDuino     |      |   Parser          |      |   MQTT Broker     |
|   Hardware        |<-----|   (SDProtocols)   |<-----|   (extern)        |
+-------------------+      +-------------------+      +-------------------+
  • Transport: Abstrahiert die physikalische Verbindung (asynchrone Lese-/Schreiboperationen).

  • Controller: Orchestriert die drei Haupt-Tasks und verwaltet die Queues.

  • Parser: Wendet die Protokoll‑Definitions‑JSON an und dekodiert Rohdaten.

  • MQTT Publisher: Stellt die Verbindung zum Broker her, publiziert Nachrichten und empfängt Kommandos.

Datenfluss mit asynchronen Queues

  1. Empfang: Hardware sendet Rohdaten → Transport liest Zeile → Reader‑Task legt Zeile in _raw_message_queue.

  2. Verarbeitung: Parser‑Task entnimmt Zeile, erkennt Protokoll, dekodiert Nachricht.

  3. Ausgabe: Dekodierte Nachricht wird an message_callback übergeben und/oder via MQTT publiziert.

  4. Kommando: Externe Quelle (MQTT oder API) ruft send_command auf → Kommando landet in _write_queue → Writer‑Task sendet es an Hardware.

  5. Antwort: Falls Antwort erwartet wird, wartet der Controller auf das passende Event in _pending_responses.

Alle Schritte sind asynchron und nicht‑blockierend; Tasks können parallel laufen, solange die Queues nicht leer sind.

Migration von Threading zu Asyncio

Die Architektur wurde von einer threading‑basierten Implementierung (Version 0.8.x) zu einer reinen asyncio‑Implementierung migriert. Wichtige Änderungen:

  • Ersetzung von threading.Thread durch asyncio.Task

  • Ersetzung von queue.Queue durch asyncio.Queue

  • Ersetzung von threading.Event durch asyncio.Event

  • async/await in allen E/A‑Methoden

  • Asynchrone Kontextmanager für Ressourcenverwaltung

Details zur Migration sind im Dokument ASYNCIO_MIGRATION.md zu finden.

Dokumentations-Infrastruktur (Sitemap & SEO)

Die PySignalduino-Dokumentation wird automatisch mit einer dynamischen Sitemap und branch-spezifischen robots.txt-Dateien versehen, um die Auffindbarkeit in Suchmaschinen zu verbessern.

Sitemap-Generierung

Die Sitemap wird durch das Python-Skript tools/generate_sitemap.py generiert, das:

  1. Den Build-Output-Ordner (build/site/html) nach HTML-Dateien scannt

  2. Prioritäten (0.1–1.0) und Update-Frequenzen (changefreq) basierend auf Dateipfaden zuweist

  3. Branch-spezifische Base-URLs unterstützt (main: pysignalduino.rfd-fhem.github.io, preview: preview.rfd-fhem.github.io)

  4. Gültige XML-Sitemap gemäß sitemaps.org-Schema generiert

  5. Fehlerbehandlung und Logging enthält

Das Skript kann manuell ausgeführt werden:

python tools/generate_sitemap.py --build-dir build/site/html --output sitemap.xml --branch main

robots.txt-Konfiguration

Die Datei docs/robots.txt wird im CI/CD-Workflow branch-spezifisch angepasst:

  • main-Branch: Erlaubt Crawling aller Pfade, schließt Preview-/Develop-Pfade aus

  • preview-Branch: Verbietet Crawling des /preview/-Pfads

  • develop-Branch: Verbietet Crawling des /develop/-Pfads

Zusätzlich wird ein Crawl-delay: 2 gesetzt, um Serverlast zu reduzieren.

Integration in den CI/CD-Workflow

Der GitHub Actions Workflow .github/workflows/docs.yml wurde erweitert, um:

  1. Nach dem Asciidoctor-Build das Sitemap-Generierungsskript auszuführen

  2. Die robots.txt in das Ausgabeverzeichnis zu kopieren und branch-spezifisch anzupassen

  3. Bei Fehlern der Sitemap-Generierung nicht den gesamten Build fehlschlagen zu lassen (continue-on-error: true)

Statische Fallback-Dateien

Für den Fall, dass die dynamische Generierung fehlschlägt, stehen statische Vorlagen bereit:

  • docs/sitemap_template.xml – Grundlegende Sitemap mit den wichtigsten URLs

  • docs/robots.txt – Generische robots.txt-Vorlage

Diese Dateien werden automatisch durch den CI/CD-Workflow verwendet.

SEO-Empfehlungen

Beitrag leisten (Contributing)

Note

Da PySignalduino noch in aktiver Entwicklung ist, können sich Code-Strukturen und APIs schnell ändern. Bitte synchronisieren Sie Ihren Fork regelmäßig mit dem upstream-Repository.

Beiträge zum Projekt sind willkommen!

Workflow

  1. Fork & Clone: Projekt forken und lokal klonen.

  2. Branch: Feature-Branch erstellen (git checkout -b feature/mein-feature).

  3. Entwicklung: Änderungen implementieren.

  4. Tests: Sicherstellen, dass alle Tests bestehen (pytest).

  5. Pull Request: PR auf GitHub öffnen.

Entwicklungsumgebung

Abhängigkeiten installieren

Das Projekt verwendet poetry für die Abhängigkeitsverwaltung. Installieren Sie die Entwicklungsabhängigkeiten mit:

Unresolved directive in 02_developer_guide/contribution.adoc - include::../../examples/bash/install_dev_deps.sh[]

Oder verwenden Sie poetry install (falls Poetry konfiguriert ist).

Die wichtigsten Entwicklungsabhängigkeiten sind:

  • pytest – Testframework

  • pytest-mock – Mocking-Unterstützung

  • pytest-asyncio – Asyncio-Testunterstützung

  • pytest-cov – Code-Coverage

  • aiomqtt – Asynchrone MQTT-Client-Bibliothek (für Tests gemockt)

Code-Stil und Linting

Das Projekt folgt PEP 8. Verwenden Sie black für automatische Formatierung und ruff für Linting.

Unresolved directive in 02_developer_guide/contribution.adoc - include::../../examples/bash/format_code.sh[]

Es gibt keine strikte CI-Prüfung, aber konsistenter Stil wird erwartet.

Tests ausführen

Das Projekt nutzt pytest. Stellen Sie sicher, dass requirements-dev.txt installiert ist.

Unresolved directive in 02_developer_guide/contribution.adoc - include::../../examples/bash/run_pytest.sh[]

Für spezifische Testmodule:

Unresolved directive in 02_developer_guide/contribution.adoc - include::../../examples/bash/run_specific_tests.sh[]

Asyncio-Tests

Seit der Migration zu asyncio (Version 0.9.0) sind alle Tests asynchron und verwenden pytest-asyncio. Testfunktionen müssen mit @pytest.mark.asyncio dekoriert sein und async def verwenden.

Beispiel:

@pytest.mark.asyncio
async def test_send_command_fire_and_forget(mock_transport, mock_parser):
    """Test sending a command without expecting a response."""
    controller = SignalduinoController(transport=mock_transport, parser=mock_parser)
    async with controller:
        # Manually check queue without starting tasks
        await controller.send_command("V")
        cmd = await controller._write_queue.get()
        assert cmd.payload == "V"
        assert not cmd.expect_response

Mocking asynchroner Objekte

Verwenden Sie AsyncMock aus unittest.mock, um asynchrone Methoden zu mocken. Achten Sie darauf, asynchrone Kontextmanager (aenter, aexit) korrekt zu mocken.

def mock_transport():
    """Fixture for a mocked async transport layer."""
    transport = AsyncMock()
    transport.is_open = True
    transport.write_line = AsyncMock()
    
    async def aopen_mock():
        transport.is_open = True
    
    async def aclose_mock():
        transport.is_open = False

    transport.aopen.side_effect = aopen_mock
    transport.aclose.side_effect = aclose_mock
    transport.__aenter__.return_value = transport
    transport.__aexit__.return_value = None
    transport.readline.return_value = None
    return transport

In Fixtures (siehe tests/conftest.py) werden Transport- und MQTT-Client-Mocks bereitgestellt.

Test-Coverage

Coverage-Bericht generieren:

Unresolved directive in 02_developer_guide/contribution.adoc - include::../../examples/bash/coverage_report.sh[]

Der Bericht wird im Verzeichnis htmlcov/ erstellt.

Code-Stil und Best Practices für asyncio

Allgemeine Richtlinien

  • Verwenden Sie async/await für alle E/A-Operationen.

  • Vermeiden Sie blockierende Aufrufe (z.B. time.sleep, synchrones Lesen/Schreiben) in asynchronen Kontexten. Nutzen Sie stattdessen asyncio.sleep.

  • Nutzen Sie asynchrone Iteratoren (async for) und Kontextmanager (async with), wo passend.

Asynchrone Queues

  • Verwenden Sie asyncio.Queue für die Kommunikation zwischen Tasks.

  • Achten Sie auf korrekte Behandlung von Queue.task_done() und await queue.join().

  • Setzen Sie angemessene Timeouts, um Deadlocks zu vermeiden.

Fehlerbehandlung

  • Fangen Sie asyncio.CancelledError in Tasks, um saubere Beendigung zu ermöglichen.

  • Verwenden Sie asyncio.TimeoutError für Timeouts bei asyncio.wait_for.

  • Protokollieren Sie Ausnahmen mit logger.exception in except-Blöcken.

Ressourcenverwaltung

  • Implementieren Sie aenter/aexit für Ressourcen, die geöffnet/geschlossen werden müssen (Transport, MQTT-Client).

  • Stellen Sie sicher, dass aexit auch bei Ausnahmen korrekt aufgeräumt wird.

Performance

  • Vermeiden Sie das Erstellen zu vieler gleichzeitiger Tasks; nutzen Sie asyncio.gather mit angemessener Begrenzung.

  • Verwenden Sie asyncio.create_task für Hintergrundtasks, aber behalten Sie Referenzen, um sie später abbrechen zu können.

Pull-Request Prozess

  1. Vor dem Einreichen: Stellen Sie sicher, dass Ihr Branch auf dem neuesten Stand von main ist und alle Tests bestehen.

  2. Beschreibung: Geben Sie im PR eine klare Beschreibung der Änderungen, des Problems und der Lösung an.

  3. Review: Mindestens ein Maintainer muss den PR reviewen und genehmigen.

  4. Merge: Nach Genehmigung wird der PR gemergt (Squash-Merge bevorzugt).

Checkliste für PRs

  • ❏ Tests hinzugefügt/aktualisiert und alle bestehenden Tests bestehen.

  • ❏ Code folgt PEP 8 (Black/Ruff).

  • ❏ Dokumentation aktualisiert (falls nötig).

  • ❏ Keine neuen Warnungen oder Fehler im Linter.

  • ❏ Changelog aktualisiert (optional, wird vom Maintainer übernommen).

AI‑Agenten Richtlinien

Für AI‑Agenten, die Code oder Systemkonfigurationen ändern, gelten zusätzliche verbindliche Vorgaben. Jede Änderung muss eine vollständige Analyse der Auswirkungen auf die zugehörige Dokumentation und die Testsuite umfassen.

Die detaillierten Richtlinien sind in AGENTS.md dokumentiert. Die wichtigsten Pflichten sind:

  • Dokumentationspflicht: Die Dokumentation muss synchron zu allen vorgenommenen Änderungen aktualisiert werden. Betroffen sind das docs/‑Verzeichnis, Inline‑Kommentare, Docstrings, README.md und andere Markdown‑Dateien.

  • Test‑Pflicht: Bestehende Tests sind zu überprüfen und anzupassen; bei Bedarf sind neue Tests zu erstellen, um eine vollständige Testabdeckung der neuen oder modifizierten Logik zu gewährleisten.

  • Verbindlichkeit: Diese Praxis ist für jede Änderung verbindlich und nicht verhandelbar. Ein Commit, der die Dokumentation oder Tests nicht entsprechend anpasst, ist unzulässig.

Vor dem Commit ist die Checkliste in AGENTS.md (Abschnitt „Mandatory Documentation and Test Maintenance“) abzuarbeiten.

Hinweise für Protokoll-Entwicklung

Falls Sie ein neues Funkprotokoll hinzufügen möchten:

  1. Fügen Sie die Definition in sd_protocols/protocols.json hinzu.

  2. Implementieren Sie die Dekodierungsmethode in der entsprechenden Mixin-Klasse (ManchesterMixin, PostdemodulationMixin, etc.).

  3. Schreiben Sie Tests für das Protokoll in tests/test_manchester_protocols.py oder ähnlich.

  4. Dokumentieren Sie das Protokoll in docs/03_protocol_reference/protocol_details.adoc.

Weitere Details finden Sie in der Architektur-Dokumentation (architecture.adoc).

Protokolldetails

PySignalduino unterstützt eine Vielzahl von Funkprotokollen im 433 MHz und 868 MHz Bereich.

Protokolldefinition

Die Datei sd_protocols/protocols.json ist die definitive Quelle für alle Protokollparameter (Timings, Preambles, Methoden).

Auszug unterstützter Protokolle

  • ID 10: Oregon Scientific v2/v3 (Manchester, 433 MHz)

  • ID 13: Flamingo FA21 Rauchmelder

  • ID 58: TFA Wettersensoren

  • ID 70: FHT80TF Tür-/Fensterkontakt (868 MHz)

  • ID 80: EM1000WZ Energiemonitor

Protokoll-Typen

  • Manchester: Selbsttaktend (z.B. Oregon, TFA).

  • TwoState / PWM: Kodierung über Pulslängen.

  • FSK: Frequenzumtastung (oft bei 868 MHz Geräten wie Lacrosse).

Neues Protokoll hinzufügen

  1. Definition in protocols.json ergänzen.

  2. Dekodierungsmethode implementieren (z.B. in sd_protocols/manchester.py).

  3. Tests hinzufügen.