LED Magic Home Streifen in Gladys über MQTT integrieren

Hallo,
Zuerst möchte ich klarstellen, dass ich in vielen Bereichen ein recht fortgeschrittenes technisches Profil habe, aber ich bin kein Entwickler. Ich bin jedoch in den letzten Monaten zum Vibe-Coder geworden. Manche mögen diesen Ansatz, andere nicht. Jedenfalls, wenn es sehr gut organisiert ist und man gelernt hat, sich der künstlichen Intelligenz zu bedienen, kann man wirklich nette Dinge herausbringen. Aber ich wollte es euch sagen, weil das, was ich gleich poste, von mir gemacht wurde, aber vor allem von Claude, meinem Kumpel von Anthropic.

Frisch auf Gladys angekommen, konnte ich mit einigen kleinen Opfern alles mit Z-Wave, Zigbee und Tuya wieder verbinden. Das Einzige, was noch fehlt, sind meine Magic Home LED-Streifen. Ich habe nachgeschaut, um auf Zigbee umzustellen, aber das ist ein bisschen zu komplex, besonders weil ich Module habe, die in abgehängten Decken sind usw.

Als ich herausfand, was man mit MQTT machen kann, kam mir eine Idee. Nach einigen Recherchen bin ich auf dieses GitHub gestoßen: GitHub - CasperVerswijvelt/magic-home-rest: Simple REST API to control magic-home lights on the same network · GitHub

Also kurz gesagt, ich hatte alles, was ich brauchte, um etwas Nettes zu bauen. Ich habe das in zwei Stunden gemacht und fange gerade erst an, es zu nutzen. Also weiß ich noch nicht, ob es langfristig stabil ist.

Übrigens sind die lokalen IPs in den Code-Ausschnitten nicht meine echten IPs.

Magic Home LED-Streifen in Gladys über MQTT integrieren

Das Problem

Die LED-Streifen Magic Home (auch unter den Marken Arilux, ZJ-WFMN-A/B/C usw. erhältlich) verwenden das proprietäre flux_led-Protokoll über lokales WiFi. Es gibt keine native Integration in Gladys für diese Controller.

Die üblichen Lösungen:

  • Ersetzen der Controller durch Zigbee: möglich, aber erfordert Lötarbeiten, wenn die Stecker nicht kompatibel sind (häufig der Fall bei RGBWW-Modellen mit 5 Pins) und ist umständlich, wenn die Controller in abgehängten Decken eingebaut sind

  • Auf Tasmota flashen: nicht immer möglich, je nach Chipset

Die Lösung: Ein benutzerdefinierter MQTT-Bridge

Man erstellt einen kleinen Node.js-Bridge, der die Verbindung zwischen Gladys (über MQTT) und den Magic Home-Controllern (über das lokale flux_led-Protokoll) herstellt.

Architektur

Gladys UI → MQTT (Mosquitto) → magic-home-bridge → Lokales WiFi → Magic Home-Controller → LED-Streifen
                                      ↓
                               Rückmeldung → MQTT → Gladys

Was benötigt wird

  • Gladys mit der MQTT-Integration konfiguriert und mit einem Mosquitto-Broker verbunden

  • Die Magic Home-Controller im gleichen lokalen Netzwerk wie der Gladys-Server

  • Docker zum Ausführen des Bridges

  • Die IP-Adressen Ihrer Magic Home-Controller (fixieren Sie diese in Ihrem DHCP!)

Schritt 1: Verbindungsfähigkeit testen

Zuerst überprüft man, ob die Controller antworten. Man verwendet magic-home-rest zum Testen:

# Testcontainer starten
docker run -d --name magic-home-rest --network host \
  -e PORT=8888 \
  casperverswijvelt/magic-home-rest:latest

# Einen Controller testen (IP-Adresse anpassen)
curl -X POST http://localhost:8888/api/power/ \
  -H "Content-Type: application/json" \
  -d '{"address": "192.168.1.100", "power": true}'

Wenn Sie OK erhalten, ist alles gut. Der Streifen sollte sich einschalten.

Testen Sie auch die Farbe:

curl -X POST http://localhost:8888/api/color/ \
  -H "Content-Type: application/json" \
  -d '{"address": "192.168.1.100", "color": "#ff0000"}'

Hinweis: Die automatische Erkennung (UDP-Scan) funktioniert nicht immer je nach Netzwerk. Deshalb verwendet man die IP-Adressen direkt.

Schritt 2: MQTT-Geräte in Gladys erstellen

In Gladys gehen Sie zu Integrationen → MQTT und erstellen ein Gerät pro LED-Streifen.

Für jedes Gerät erstellen Sie 3 Funktionen:

Name Kategorie Typ Sensor? Min Max
Einschalten Licht Binär (ein/aus) Nein 0 1
Helligkeit Licht Helligkeit Nein 0 100
Farbe Licht Farbe Nein 0 0

Verwenden Sie eine konsistente Namenskonvention für die externen Identifikatoren:

  • Gerät: mqtt:küche:bdled-insel

  • Funktionen: mqtt:küche:bdled-insel:power, mqtt:küche:bdled-insel:brightness, mqtt:küche:bdled-insel:color

Schritt 3: Bridge bereitstellen

Dateien erstellen

Erstellen Sie einen Ordner für den Bridge (z. B. /volume1/docker/magic-home-bridge/).

package.json:

{
  "name": "magic-home-bridge",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
    "magic-home": "^2.4.0",
    "mqtt": "^5.10.0"
  }
}

index.js :

const mqtt = require("mqtt");
const { Control } = require("magic-home");

// ============================================================
// KONFIGURATION — Passen Sie diesen Abschnitt an Ihre Installation an
// ============================================================

const MQTT_URL = process.env.MQTT_URL || "mqtt://localhost:1883";
const POLL_INTERVAL = parseInt(process.env.POLL_INTERVAL || "30000", 10);

// Listen Sie Ihre Controller hier auf:
// - extId: Die externe ID des Geräts in Gladys
// - ip: Die feste IP-Adresse des Magic Home Controllers
// - name: Ein lesbarer Name für die Logs
const DEVICES = [
  { extId: "mqtt:cuisine:bdled-ilot", ip: "192.168.1.100", name: "Kochinsel" },
  { extId: "mqtt:chambre:bdled-lit",  ip: "192.168.1.101", name: "Bett" },
  // Fügen Sie Ihre hinzu...
];

// ============================================================
// Ändern Sie nichts darunter, es sei denn, Sie wissen, was Sie tun
// ============================================================

const stateCache = {};
DEVICES.forEach(d => {
  stateCache[d.extId] = { power: 0, brightness: 100, color: 16777215 };
});

function createControl(ip) {
  return new Control(ip, { connect_timeout: 5000, command_timeout: 5000 });
}

function getScaledRGB(extId) {
  const { color, brightness } = stateCache[extId];
  const r = (color >> 16) & 0xFF;
  const g = (color >> 8) & 0xFF;
  const b = color & 0xFF;
  const scale = brightness / 100;
  return [Math.round(r * scale), Math.round(g * scale), Math.round(b * scale)];
}

function publishState(extId, feature, value) {
  client.publish(
    "gladys/master/device/" + extId + "/feature/" + extId + ":" + feature + "/state",
    String(value)
  );
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// --- Befehlsverarbeitung ---

async function handlePower(extId, ip, value) {
  const ctrl = createControl(ip);
  const on = parseInt(value, 10) === 1;
  if (on) {
    await ctrl.turnOn();
    await sleep(150);
    const [r, g, b] = getScaledRGB(extId);
    await ctrl.setColor(r, g, b);
  } else {
    await ctrl.turnOff();
  }
  stateCache[extId].power = on ? 1 : 0;
  publishState(extId, "power", stateCache[extId].power);
}

async function handleBrightness(extId, ip, value) {
  const brightness = Math.max(0, Math.min(100, parseInt(value, 10)));
  stateCache[extId].brightness = brightness;
  const ctrl = createControl(ip);
  const [r, g, b] = getScaledRGB(extId);
  await ctrl.setColor(r, g, b);
  publishState(extId, "brightness", brightness);
}

async function handleColor(extId, ip, value) {
  const colorInt = parseInt(value, 10);
  stateCache[extId].color = colorInt;
  const ctrl = createControl(ip);
  const [r, g, b] = getScaledRGB(extId);
  await ctrl.setColor(r, g, b);
  publishState(extId, "color", colorInt);
}

// --- Periodische Abfrage ---

async function pollAllDevices() {
  for (const dev of DEVICES) {
    try {
      const ctrl = createControl(dev.ip);
      const state = await ctrl.queryState();

      const power = state.on ? 1 : 0;
      stateCache[dev.extId].power = power;
      publishState(dev.extId, "power", power);

      const { red, green, blue } = state.color;
      const colorInt = (red << 16) | (green << 8) | blue;
      publishState(dev.extId, "color", colorInt);

      const maxC = Math.max(red, green, blue);
      const brightness = maxC > 0 ? Math.round((maxC / 255) * 100) : 0;
      stateCache[dev.extId].brightness = brightness;
      publishState(dev.extId, "brightness", brightness);

      console.log("[POLL] " + dev.name + ": power=" + power +
        " color=#" + colorInt.toString(16).padStart(6, "0") +
        " brightness=" + brightness + "%");
    } catch (err) {
      console.error("[POLL ERR] " + dev.name + ":", err.message);
    }
  }
}

// --- MQTT-Verbindung ---

const client = mqtt.connect(MQTT_URL, {
  clientId: "magic-home-bridge",
  clean: true,
  reconnectPeriod: 5000,
});

client.on("connect", () => {
  console.log("[MQTT] Verbunden mit " + MQTT_URL);

  for (const dev of DEVICES) {
    const base = "gladys/device/" + dev.extId + "/feature/" + dev.extId;
    client.subscribe(base + ":power/state");
    client.subscribe(base + ":brightness/state");
    client.subscribe(base + ":color/state");
  }
  console.log("[MQTT] Abonniert auf " + (DEVICES.length * 3) + " Topics");

  setTimeout(pollAllDevices, 2000);
  setInterval(pollAllDevices, POLL_INTERVAL);
});

client.on("message", async (topic, message) => {
  const value = message.toString().trim();
  const match = topic.match(/^gladys\/device\/([^/]+)\/feature\/([^/]+)\/state$/);
  if (!match) return;

  const deviceExtId = match[1];
  const featureExtId = match[2];
  const featureType = featureExtId.split(":").pop();
  const dev = DEVICES.find(d => d.extId === deviceExtId);
  if (!dev) return;

  console.log("[CMD] " + dev.name + " > " + featureType + " = " + value);

  try {
    switch (featureType) {
      case "power": await handlePower(deviceExtId, dev.ip, value); break;
      case "brightness": await handleBrightness(deviceExtId, dev.ip, value); break;
      case "color": await handleColor(deviceExtId, dev.ip, value); break;
    }
  } catch (err) {
    console.error("[ERR] " + dev.name + " " + featureType + ":", err.message);
  }
});

client.on("error", (err) => console.error("[MQTT ERR]", err.message));
client.on("reconnect", () => console.log("[MQTT] Wiederverbindung..."));

process.on("SIGTERM", () => { client.end(); process.exit(0); });
process.on("SIGINT", () => { client.end(); process.exit(0); });

console.log("[BRIDGE] Magic Home MQTT Bridge v1.0");
console.log("[BRIDGE] " + DEVICES.length + " Geräte, Abfrage alle " + (POLL_INTERVAL / 1000) + "s");

Abhängigkeiten installieren

docker run --rm \
  -v /pfad/zu/magic-home-bridge:/app \
  -w /app \
  node:22-alpine npm install --production

Bridge starten

docker run -d \
  --name magic-home-bridge \
  --network host \
  --restart unless-stopped \
  -v /pfad/zu/magic-home-bridge:/app \
  -w /app \
  -e MQTT_URL=mqtt://localhost:1883 \
  -e POLL_INTERVAL=30000 \
  node:22-alpine \
  node index.js

Logs überprüfen

docker logs magic-home-bridge

Sie sollten sehen:

[BRIDGE] Magic Home MQTT Bridge v1.0
[BRIDGE] 2 Geräte, Abfrage alle 30s
[MQTT] Verbunden mit mqtt://localhost:1883
[MQTT] Abonniert auf 6 Topics
[POLL] Kochinsel: power=1 color=#ff6e54 brightness=100%
[POLL] Bett: power=0 color=#000000 brightness=0%

Wie es funktioniert

Befehlsfluss (Gladys → LED)

  1. Sie klicken auf « Anschalten » im Gladys-Dashboard

  2. Gladys veröffentlicht 1 auf dem MQTT-Topic gladys/device/mqtt:cuisine:bdled-ilot/feature/mqtt:cuisine:bdled-ilot:power/state

  3. Der Bridge empfängt die Nachricht und ruft Control.turnOn() auf der IP des Controllers auf

  4. Der Controller schaltet das LED-Band über lokales WiFi ein

  5. Der Bridge veröffentlicht den bestätigten Zustand auf gladys/master/device/.../state

  6. Gladys aktualisiert die Benutzeroberfläche

Zustandsfluss (LED → Gladys)

Alle 30 Sekunden fragt der Bridge jeden Controller über TCP ab und veröffentlicht den Zustand (power, Farbe, Helligkeit) an Gladys. Dies ermöglicht:

  • Synchronisierung des Zustands, wenn jemand die Magic Home-App verwendet

  • Erkennung von ausgeschalteten/abgemeldeten Controllern

Farb- + Helligkeitsverwaltung

Die Magic Home Controller haben keinen separaten Helligkeitskanal — alles läuft über die RGB-Werte. Der Bridge verwaltet dies:

  • Gladys sendet die Farbe als Ganzzahl (z. B. 16711680 = reines Rot)

  • Gladys sendet die Helligkeit als Prozentsatz (0-100)

  • Der Bridge multipliziert die RGB-Komponenten mit dem Helligkeitsprozentsatz, bevor er sie an den Controller sendet

Bekannte Einschränkungen

  • Kein Warm White / Cold White: Der Bridge verwaltet derzeit nur RGB. Wenn Ihre Bänder RGBWW (5 Kanäle) sind, werden die Warm White/Cold White Kanäle nicht gesteuert

  • Ungefähre Helligkeit: Da die Helligkeit durch Skalieren von RGB simuliert wird, ist die Genauigkeit beim Rücklauf (Abfrage) nicht perfekt

  • Keine automatische Erkennung: Sie müssen die IP-Adressen Ihrer Controller kennen und sie im DHCP festlegen* Nur ein Controller zur gleichen Zeit: Befehle werden sequenziell gesendet (kein Problem in der Praxis, es ist instantan)

Voraussetzungen

  • Gladys mit konfigurierter MQTT-Integration

  • Mosquitto (oder ein anderer MQTT-Broker)

  • Docker mit Netzwerkzugriff auf den Host

  • Magic Home Controller mit fester IP im gleichen LAN

Erfolgreich getestet auf einem Synology NAS DS1520+ (Docker) mit Gladys v4 und 5 RGBWW-LED-Streifen.

Super Tuto @David-Digitis :ok_hand:
Ich habe mir erlaubt, die Kategorie in … Tutorials :wink: zu ändern

Danke, das hast du gut gemacht :wink: