LED-Beleuchtung im Serverschrank bietet nicht nur ein optisches Upgrade, sondern kann auch funktionale Vorteile wie Statusanzeigen oder einfachere Wartung bringen. Besonders interessant wird es, wenn handelsübliche, günstige Komponenten in ein professionelles System integriert werden – so wie im folgenden Projekt, bei dem eine 6-Euro-LED-Leiste mit Bluetooth-Ansteuerung vollständig lokal steuerbar gemacht wurde.
Reverse Engineering der Bluetooth-Kommunikation
Die mitgelieferte Fernbedienung der LED-Leiste funktionierte zwar, doch die Bluetooth-Funktionalität war exklusiv an die proprietäre "illumi-home"-App gebunden – ein No-Go für ein kontrolliertes Homelab. Um die Kommunikation zu analysieren, wurde zunächst versucht, den Datenverkehr auf iOS mit nRF Connect zu sniffen – allerdings scheiterte dieser Ansatz an Systembeschränkungen. Auch Android erwies sich als wenig hilfreich. Die Lösung: Der Einsatz von PacketLogger im macOS-Entwicklermodus. Mit einem aktivierten Bluetooth-Debug-Profil konnten die relevanten Protokollinhalte identifiziert werden: Handshake-Kommandos, Keep-Alive-Signale und Farbsteuerbefehle.
Effiziente Steuerung durch Python-Service
Ziel war die direkte Steuerung über ein Python-Skript auf einem NUC-Server. Erste Tests offenbarten jedoch eine störende Latenz, verursacht durch die wiederholte Verbindungsaufnahme bei jedem einzelnen Farbwechsel. Abhilfe schuf eine persistente Client-Server-Architektur: Ein kontinuierlich laufender Service-Worker hielt die Bluetooth-Verbindung offen, während ein leichtgewichtiges Client-Skript lediglich Befehle sendete. Zusätzliche Stabilität brachte der Austausch des internen NUC-Bluetooth-Moduls gegen einen externen USB-Adapter mit CSR-Chip – seitdem funktioniert die Verbindung auch bei kurzen Distanzen zuverlässig.
Service-Worker#!/usr/bin/env python3 import asyncio import os import sys import socket from bleak import BleakClient, BleakError from datetime import datetime from typing import Optional # Konfiguration DEVICE_ADDRESS = "00:00:00:00:00:00" #BT-Adresse der LED-Leiste CHAR_UUID = "00000000-0000-0000-0000-000000000000" #UUID der Write-Befehle (kann einfach über nRF-Connect ausgelesen werden) SOCKET_PATH = "/tmp/led_ble.sock" KEEPALIVE_COMMAND = bytes.fromhex("5A0102FF") ADAPTER_NAME = "hci0" #BT Adapter-Name des NUCs # Optimierte Intervalle KEEPALIVE_INTERVAL = 60 BLE_OPERATION_TIMEOUT = 5 CONNECTION_TIMEOUT = 8 # Retry-Logik INITIAL_RETRY_DELAY = 1 MAX_RETRY_DELAY = 15 MAX_RETRIES_BEFORE_RESET = 2 class Colors: GREEN = "�33[92m" YELLOW = "�33[93m" RED = "�33[91m" BLUE = "�33[94m" RESET = "�33[0m" def log(message: str, level: str = "info"): timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] color = { "info": Colors.GREEN, "warning": Colors.YELLOW, "error": Colors.RED, "debug": Colors.BLUE }.get(level.lower(), Colors.RESET) print(f"{color}[{timestamp}] {message}{Colors.RESET}") async def handle_socket(client: BleakClient, stop_event: asyncio.Event): if os.path.exists(SOCKET_PATH): os.remove(SOCKET_PATH) server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) server.setblocking(False) server.bind(SOCKET_PATH) server.listen(1) log("???? Warte auf Befehle via Socket", "info") try: while not stop_event.is_set(): try: conn, _ = await asyncio.get_event_loop().sock_accept(server) with conn: data = await asyncio.get_event_loop().sock_recv(conn, 1024) if not data: continue try: payload = bytes.fromhex(data.decode().strip()) await client.write_gatt_char(CHAR_UUID, payload) log(f"✅ Befehl gesendet: {data.decode().strip()}", "info") except Exception as e: log(f"❌ Fehler beim Senden: {e}", "error") except Exception as e: if not stop_event.is_set(): log(f"⚠️ Socket Fehler: {e}", "warning") await asyncio.sleep(0.5) finally: server.close() if os.path.exists(SOCKET_PATH): os.remove(SOCKET_PATH) async def keepalive_loop(client: BleakClient, stop_event: asyncio.Event): log("???? Keep-Alive gestartet", "info") while not stop_event.is_set(): try: await client.write_gatt_char(CHAR_UUID, KEEPALIVE_COMMAND) log("???? Keep-Alive gesendet", "debug") except Exception as e: log(f"⚠️ Keep-Alive Fehler: {e}", "error") stop_event.set() return await asyncio.sleep(KEEPALIVE_INTERVAL) async def connection_monitor(client: BleakClient, stop_event: asyncio.Event): log("???? Verbindungsmonitor gestartet", "info") while not stop_event.is_set(): if not client.is_connected: log("⚠️ Verbindung verloren", "error") stop_event.set() return await asyncio.sleep(1) async def reset_bluetooth(): log("♻️ Reset Bluetooth-Adapter...", "warning") os.system("sudo hciconfig hci0 down") await asyncio.sleep(1) os.system("sudo hciconfig hci0 up") await asyncio.sleep(2) log("✅ Bluetooth-Adapter zurückgesetzt", "info") async def manage_connection(): retry_count = 0 while True: current_delay = min(INITIAL_RETRY_DELAY * (2 ** retry_count), MAX_RETRY_DELAY) if retry_count > 0: log(f"⏳ Warte {current_delay}s... (Versuch {retry_count + 1})", "info") await asyncio.sleep(current_delay) if retry_count >= MAX_RETRIES_BEFORE_RESET: await reset_bluetooth() retry_count = 0 # Reset nach Adapter-Reset try: async with BleakClient( DEVICE_ADDRESS, adapter=ADAPTER_NAME, # <-- Hier TP-Link Adapter erzwingen timeout=CONNECTION_TIMEOUT ) as client: if client.is_connected: log(f"✅ Verbunden mit {DEVICE_ADDRESS}", "info") # Initialer Wakeup try: await client.write_gatt_char(CHAR_UUID, KEEPALIVE_COMMAND) log("???? Initialbefehl gesendet", "info") except Exception as e: log(f"⚠️ Initialbefehl fehlgeschlagen: {e}", "warning") continue # Tasks starten stop_event = asyncio.Event() tasks = [ asyncio.create_task(handle_socket(client, stop_event)), asyncio.create_task(keepalive_loop(client, stop_event)), asyncio.create_task(connection_monitor(client, stop_event)) ] # Warte auf Stop-Signal await stop_event.wait() # Tasks beenden for task in tasks: task.cancel() await asyncio.gather(*tasks, return_exceptions=True) retry_count = 0 # Reset bei Erfolg except asyncio.TimeoutError: log("⌛ Timeout beim Verbindungsaufbau", "warning") except BleakError as e: log(f"❌ BLE Fehler: {e}", "error") except Exception as e: log(f"❌ Unerwarteter Fehler: {e}", "error") retry_count += 1 async def main(): try: # Initialer Bluetooth-Reset await reset_bluetooth() await manage_connection() except KeyboardInterrupt: log(" ???? Beendet durch Benutzer", "info") finally: if os.path.exists(SOCKET_PATH): os.remove(SOCKET_PATH) if __name__ == "__main__": try: asyncio.run(main()) except KeyboardInterrupt: log(" ???? Beendet durch Benutzer", "info") sys.exit(0)Web-Dienst auf Port 3000
#!/usr/bin/env python3 import socket from http.server import BaseHTTPRequestHandler, HTTPServer SOCKET_PATH = "/tmp/led_ble.sock" BEFEHLE = { "blue": "5A07010041FF", # BLAU "green": "5A070100FF00", # GRÜN "red": "5A0701FF0000" # ROT } class LEDRequestHandler(BaseHTTPRequestHandler): def do_GET(self): try: color = self.path.strip("/").lower() if color in BEFEHLE: self.send_command(BEFEHLE[color]) self.send_response(200) self.end_headers() self.wfile.write(f"Erfolg: {color}".encode()) else: self.send_error(400, "Ungültige Farbe (blue/green/red/off)") except Exception as e: self.send_error(500, str(e)) def send_command(self, hex_code: str): with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: sock.connect(SOCKET_PATH) sock.sendall(hex_code.encode()) if __name__ == "__main__": print("???? Starte LED-Webserver auf Port 3000") server = HTTPServer(("0.0.0.0", 3000), LEDRequestHandler) server.serve_forever()
Automatisierung und Smart-Home-Integration
Die LED-Leiste dient nun auch als Netzwerkstatus-Anzeige. Ein weiteres Skript prüft im Minutentakt die Erreichbarkeit kritischer Dienste und passt die LED-Farbe entsprechend an: Blau signalisiert Normalbetrieb, Rot weist auf Störungen hin, und Grün bestätigt die Wiederverfügbarkeit. Zusätzlich wurde ein Python-basierter Webhook entwickelt, der über Port 3000 HTTP-Farbbefehle entgegennimmt. Mittels Homebridge und dem Plugin homebridge-http-switch wurden drei virtuelle Schalter konfiguriert, um die LEDs auch über Apple Home zu steuern – ein gelungener Brückenschlag zwischen Bastelprojekt und Smart-Home-System.
Cronjob zur automatischen Überwachung als Bash#!/bin/bash # Konfiguration STATUS_FILE="/tmp/led_status" LOG_FILE="/tmp/led_debug" LED_BASE_URL="http://192.168.99.101:3000" CURL_OPTIONS="--connect-timeout 2 --max-time 4 -s" TEST_IPS=("1.1.1.1" "192.168.99.1" "192.168.99.100") INTERFACE="enp1s0" # Status laden LAST_STATUS=$(cat "$STATUS_FILE" 2>/dev/null || echo "unknown") # Logging-Funktion log() { echo "[$(date "+%Y-%m-%d %H:%M:%S")] $1" >> "$LOG_FILE" } # LED setzen set_led() { local color="$1" curl $CURL_OPTIONS "$LED_BASE_URL/$color" && echo "$color" > "$STATUS_FILE" } # Prüfe, ob Interface existiert if ! ip link show "$INTERFACE" &>/dev/null; then if [[ "$LAST_STATUS" != "red" ]]; then set_led red && log "???? Interface $INTERFACE nicht gefunden → ROT gesetzt" fi exit 0 fi # Prüfe, ob Interface physisch verbunden ist if ! ip link show "$INTERFACE" | grep -q "LOWER_UP"; then if [[ "$LAST_STATUS" != "red" ]]; then set_led red && log "???? Interface $INTERFACE physisch nicht verbunden (kein Link) → ROT gesetzt" fi exit 0 fi # Prüfe, ob Interface DOWN ist if ip link show "$INTERFACE" | grep -q "state DOWN"; then if [[ "$LAST_STATUS" != "red" ]]; then set_led red && log "???? Interface $INTERFACE DOWN → ROT gesetzt" fi exit 0 fi # Prüfe, ob IP vergeben wurde if ! ip addr show "$INTERFACE" | grep -q "inet "; then if [[ "$LAST_STATUS" != "red" ]]; then set_led red && log "???? Keine IP auf $INTERFACE → ROT gesetzt" fi exit 0 fi # Ping-Tests durchführen connection_ok=true for ip in "${TEST_IPS[@]}"; do if ! ping -c1 -W1 "$ip" >/dev/null 2>&1; then connection_ok=false break fi done # Entscheidung basierend auf Status if ! $connection_ok; then if [[ "$LAST_STATUS" != "red" ]]; then set_led red && log "???? Alle Pings fehlgeschlagen → ROT gesetzt" fi exit 0 fi # Verbindung ist wieder da → Übergang rot → grün → blau if [[ "$LAST_STATUS" == "red" ]]; then set_led green && log "???? Verbindung wieder da → GRÜN gesetzt" exit 0 fi if [[ "$LAST_STATUS" == "green" ]]; then set_led blue && log "???? Übergang abgeschlossen → BLAU gesetzt" exit 0 fi # Status ist stabil blau if [[ "$LAST_STATUS" != "blue" ]]; then set_led blue && log "???? Initial auf BLAU gesetzt" fi exit 0
Fazit
Mit etwas Reverse Engineering und cleverem Software-Design lässt sich selbst preiswerte Consumer-Hardware effektiv in professionelle Homelab-Umgebungen integrieren. Die LED-Leiste agiert nicht nur als dekoratives Element, sondern übernimmt dank individueller Steuerung und Statusanzeige auch funktionale Aufgaben – ein Paradebeispiel für die kreative Nutzung proprietärer Technik im Open-Source-Geist.
![]() |
![]() |