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.
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.pmin 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
asynciofü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:
-
Asyncio-Migrationsleitfaden – Detaillierte Anleitung zur Anpassung Ihrer Skripte und Callbacks.
-
Manchester-Migrationsleitfaden – Informationen zur Integration der Manchester‑Protokoll‑Verarbeitung.
-
Methoden‑Migrations‑Übersicht – Liste aller geänderten Methoden und Klassen.
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 (ersetztpaho-mqttin 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:
-
Die [Schnellstart-Anleitung](../index.adoc#_schnellstart) befolgen.
-
Die [Konfiguration über Umgebungsvariablen](../usage.adoc#_konfiguration) einrichten.
-
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
SignalduinoControllerbereitgestellt) -
Alle Methoden sind asynchron (
async def) und geben entwederstr(Antwort) zurück oderNone(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_HOSTundMQTT_PORTkorrekt 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 undasyncio.run()verwendet wird. Verwenden Sieasync withfür Context-Manager. -
Tasks hängen oder werden nicht abgebrochen: Alle Hintergrundtasks sollten auf das
_stop_eventreagieren. Bei manuell erstellten Tasks müssen Sieasyncio.CancelledErrorabfangen 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 Sieasyncio.wait_formit 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 Sieaiomqtt>=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.ProactorEventLoopoder 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_taskmit 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_queueab. -
Parser-Task: Entnimmt Rohzeilen aus der Queue, dekodiert sie über den
SignalParserund veröffentlicht Ergebnisse via MQTT oder ruft denmessage_callbackauf. -
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‑SerialisierteDecodedMessage-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
-
Empfang: Hardware sendet Rohdaten → Transport liest Zeile → Reader‑Task legt Zeile in
_raw_message_queue. -
Verarbeitung: Parser‑Task entnimmt Zeile, erkennt Protokoll, dekodiert Nachricht.
-
Ausgabe: Dekodierte Nachricht wird an
message_callbackübergeben und/oder via MQTT publiziert. -
Kommando: Externe Quelle (MQTT oder API) ruft
send_commandauf → Kommando landet in_write_queue→ Writer‑Task sendet es an Hardware. -
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.Threaddurchasyncio.Task -
Ersetzung von
queue.Queuedurchasyncio.Queue -
Ersetzung von
threading.Eventdurchasyncio.Event -
async/awaitin 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:
-
Den Build-Output-Ordner (
build/site/html) nach HTML-Dateien scannt -
Prioritäten (0.1–1.0) und Update-Frequenzen (
changefreq) basierend auf Dateipfaden zuweist -
Branch-spezifische Base-URLs unterstützt (main:
pysignalduino.rfd-fhem.github.io, preview:preview.rfd-fhem.github.io) -
Gültige XML-Sitemap gemäß sitemaps.org-Schema generiert
-
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:
-
Nach dem Asciidoctor-Build das Sitemap-Generierungsskript auszuführen
-
Die robots.txt in das Ausgabeverzeichnis zu kopieren und branch-spezifisch anzupassen
-
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
-
Die Sitemap wird unter
https://pysignalduino.rfd-fhem.github.io/sitemap.xmlverfügbar sein -
Die robots.txt wird unter
https://pysignalduino.rfd-fhem.github.io/robots.txtverfügbar sein -
Es wird empfohlen, die Sitemap in der Google Search Console und Bing Webmaster Tools einzureichen
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
-
Fork & Clone: Projekt forken und lokal klonen.
-
Branch: Feature-Branch erstellen (
git checkout -b feature/mein-feature). -
Entwicklung: Änderungen implementieren.
-
Tests: Sicherstellen, dass alle Tests bestehen (
pytest). -
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/awaitfür alle E/A-Operationen. -
Vermeiden Sie blockierende Aufrufe (z.B.
time.sleep, synchrones Lesen/Schreiben) in asynchronen Kontexten. Nutzen Sie stattdessenasyncio.sleep. -
Nutzen Sie asynchrone Iteratoren (
async for) und Kontextmanager (async with), wo passend.
Asynchrone Queues
-
Verwenden Sie
asyncio.Queuefür die Kommunikation zwischen Tasks. -
Achten Sie auf korrekte Behandlung von
Queue.task_done()undawait queue.join(). -
Setzen Sie angemessene Timeouts, um Deadlocks zu vermeiden.
Fehlerbehandlung
-
Fangen Sie
asyncio.CancelledErrorin Tasks, um saubere Beendigung zu ermöglichen. -
Verwenden Sie
asyncio.TimeoutErrorfür Timeouts beiasyncio.wait_for. -
Protokollieren Sie Ausnahmen mit
logger.exceptioninexcept-Blöcken.
Ressourcenverwaltung
-
Implementieren Sie
aenter/aexitfür Ressourcen, die geöffnet/geschlossen werden müssen (Transport, MQTT-Client). -
Stellen Sie sicher, dass
aexitauch bei Ausnahmen korrekt aufgeräumt wird.
Performance
-
Vermeiden Sie das Erstellen zu vieler gleichzeitiger Tasks; nutzen Sie
asyncio.gathermit angemessener Begrenzung. -
Verwenden Sie
asyncio.create_taskfür Hintergrundtasks, aber behalten Sie Referenzen, um sie später abbrechen zu können.
Pull-Request Prozess
-
Vor dem Einreichen: Stellen Sie sicher, dass Ihr Branch auf dem neuesten Stand von
mainist und alle Tests bestehen. -
Beschreibung: Geben Sie im PR eine klare Beschreibung der Änderungen, des Problems und der Lösung an.
-
Review: Mindestens ein Maintainer muss den PR reviewen und genehmigen.
-
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:
-
Fügen Sie die Definition in
sd_protocols/protocols.jsonhinzu. -
Implementieren Sie die Dekodierungsmethode in der entsprechenden Mixin-Klasse (
ManchesterMixin,PostdemodulationMixin, etc.). -
Schreiben Sie Tests für das Protokoll in
tests/test_manchester_protocols.pyoder ähnlich. -
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
-
Definition in
protocols.jsonergänzen. -
Dekodierungsmethode implementieren (z.B. in
sd_protocols/manchester.py). -
Tests hinzufügen.