Integrate Magic Home LED strips into Gladys via MQTT

Hello,

First of all, I want to make it clear that I have a fairly advanced technical profile in many areas, but I’m not a developer. However, I’ve become a vibe coder these past few months. Some people like this approach, others don’t; in any case, when it’s very well organized and you’ve properly learned how to use agentic artificial intelligences, you can produce some really nice things. I wanted to tell you that because what I’m going to post next was done by me but mainly by Claude, my friend at Anthropic.

Having recently started using Gladys, and after making a few small sacrifices, I was able to reconnect everything with Z-Wave, Zigbee and Tuya. The only important thing missing is my Magic Home LED strips. I looked into converting them to Zigbee but it’s a bit too complicated, especially since I have modules in drop ceilings, etc.

When I discovered what could be done with MQTT, it gave me ideas. With a bit of research, I came across this GitHub: GitHub - CasperVerswijvelt/magic-home-rest: Simple REST API to control magic-home lights on the same network · GitHub

So in short I had everything needed to build something nice. I did it in two hours and have only just started using it, so I don’t yet know if it’s stable in the long term.

For info, the local IPs in the code snippets are not my real IPs.

Integrer des bandeaux LED Magic Home dans Gladys via MQTT

Le probleme

Les bandeaux LED Magic Home (aussi vendus sous les marques Arilux, ZJ-WFMN-A/B/C, etc.) utilisent le protocole proprietaire flux_led en WiFi local. Il n’existe pas d’integration native dans Gladys pour ces controleurs.

Les solutions habituelles :

  • Remplacer les controleurs par du Zigbee : possible, mais implique de la soudure si les connecteurs ne sont pas compatibles (souvent le cas avec les modeles RGBWW 5 pins) et c’est galere quand les controleurs sont dans des faux plafonds

  • Flasher en Tasmota : pas toujours possible selon le chipset

La solution : un bridge MQTT custom

On cree un petit bridge Node.js qui fait le lien entre Gladys (via MQTT) et les controleurs Magic Home (via le protocole flux_led local).

Architecture

Gladys UI → MQTT (Mosquitto) → magic-home-bridge → WiFi local → Controleur Magic Home → Bandeau LED
                                      ↓
                               etat retour → MQTT → Gladys

Ce qu’il faut

  • Gladys avec l’integration MQTT configuree et connectee a un broker Mosquitto

  • Les controleurs Magic Home sur le meme reseau local que le serveur Gladys

  • Docker pour faire tourner le bridge

  • Les adresses IP de vos controleurs Magic Home (fixez-les dans votre DHCP !)

Etape 1 : Tester la connectivite

Avant tout, on verifie que les controleurs repondent. On utilise magic-home-rest pour tester :

# Lancer le container de test
docker run -d --name magic-home-rest --network host \
  -e PORT=8888 \
  casperverswijvelt/magic-home-rest:latest

# Tester un controleur (remplacez par votre IP)
curl -X POST http://localhost:8888/api/power/ \
  -H "Content-Type: application/json" \
  -d '{"address": "192.168.1.100", "power": true}'

Si vous obtenez OK, c’est bon. Le bandeau devrait s’allumer.

Testez aussi la couleur :

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

Note : la decouverte automatique (scan UDP) ne fonctionne pas toujours selon votre reseau. C’est pour ca qu’on utilise les IP directement.

Etape 2 : Creer les devices MQTT dans Gladys

Dans Gladys, allez dans Integrations → MQTT et creez un device par bandeau LED.

Pour chaque device, creez 3 fonctionnalites :

Nom Categorie Type Capteur ? Min Max
Allumer Lumiere Binaire (on/off) Non 0 1
Luminosite Lumiere Luminosite Non 0 100
Couleur Lumiere Couleur Non 0 0

Utilisez une convention de nommage coherente pour les identifiants externes :

  • Device : mqtt:cuisine:bdled-ilot

  • Features : mqtt:cuisine:bdled-ilot:power, mqtt:cuisine:bdled-ilot:brightness, mqtt:cuisine:bdled-ilot:color

Etape 3 : Deployer le bridge

Creer les fichiers

Creez un dossier pour le bridge (ex: /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");

// ============================================================
// CONFIGURATION — Adaptez cette section a votre installation
// ============================================================

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

// Listez vos controleurs ici :
// - extId : l'identifiant externe du device dans Gladys
// - ip : l'adresse IP fixe du controleur Magic Home
// - name : un nom lisible pour les logs
const DEVICES = [
  { extId: "mqtt:cuisine:bdled-ilot", ip: "192.168.1.100", name: "Ilot cuisine" },
  { extId: "mqtt:chambre:bdled-lit",  ip: "192.168.1.101", name: "Lit" },
  // Ajoutez les votres...
];

// ============================================================
// Ne modifiez rien en dessous sauf si vous savez ce que vous faites
// ============================================================

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));
}

// --- Gestion des commandes ---

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);
}

// --- Poll periodique ---

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);
    }
  }
}

// --- Connexion MQTT ---

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

client.on("connect", () => {
  console.log("[MQTT] Connecte a " + 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] Abonne a " + (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] Reconnexion..."));

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 + " devices, poll toutes les " + (POLL_INTERVAL / 1000) + "s");

Installer les dependances

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

Lancer le bridge

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

Verifier les logs

docker logs magic-home-bridge

Vous devriez voir :

[BRIDGE] Magic Home MQTT Bridge v1.0
[BRIDGE] 2 devices, poll toutes les 30s
[MQTT] Connecte a mqtt://localhost:1883
[MQTT] Abonne a 6 topics
[POLL] Ilot cuisine: power=1 color=#ff6e54 brightness=100%
[POLL] Lit: power=0 color=#000000 brightness=0%

Comment ca marche

Flux de commande (Gladys → LED)

  1. Vous cliquez sur « Allumer » dans le dashboard Gladys

  2. Gladys publie 1 sur le topic MQTT gladys/device/mqtt:cuisine:bdled-ilot/feature/mqtt:cuisine:bdled-ilot:power/state

  3. Le bridge recoit le message, appelle Control.turnOn() sur l’IP du controleur

  4. Le controleur allume le bandeau LED via WiFi local

  5. Le bridge publie l’etat confirme sur gladys/master/device/.../state

  6. Gladys met a jour l’interface

Flux d’etat (LED → Gladys)

Toutes les 30 secondes, le bridge interroge chaque controleur via TCP et publie l’etat (power, couleur, luminosite) vers Gladys. Cela permet de :

  • Synchroniser l’etat si quelqu’un utilise l’app Magic Home

  • Detecter les controleurs eteints/deconnectes

Gestion couleur + luminosite

Les controleurs Magic Home n’ont pas de canal de luminosite separe — tout passe par les valeurs RGB. Le bridge gere ca :

  • Gladys envoie la couleur comme un entier (ex: 16711680 = rouge pur)

  • Gladys envoie la luminosite comme un pourcentage (0-100)

  • Le bridge multiplie les composantes RGB par le pourcentage de luminosite avant d’envoyer au controleur

Limitations connues

  • Pas de warm white / cold white : le bridge ne gere que le RGB pour l’instant. Si vos bandeaux sont RGBWW (5 canaux), les canaux blanc chaud/froid ne sont pas controles

  • Luminosite approximative : comme la luminosite est simulee en scalant le RGB, la precision n’est pas parfaite au retour (poll)

  • Pas de decouverte automatique : vous devez connaitre les IP de vos controleurs et les fixer dans le DHCP

  • Un seul controleur a la fois : les commandes sont envoyees sequentiellement (pas de probleme en pratique, c’est instantane)

Requis

  • Gladys avec integration MQTT configuree

  • Mosquitto (ou autre broker MQTT)

  • Docker avec acces reseau host

  • Controleurs Magic Home avec IP fixe sur le meme LAN

Teste avec succes sur un NAS Synology DS1520+ (Docker) avec Gladys v4 et 5 bandeaux LED RGBWW.

5 Likes

Great tutorial @David-Digitis :ok_hand:
I took the liberty of changing the category to … Tutorials :wink:

1 Like

Thank you, you did well :wink:

Excellent! Super clean :slight_smile:

If you ever want to make it simpler, and allow other people to use this integration, you could write a small Matterbridge plugin (with the help of AI, relatively easy), that way this device would become Matter, and therefore compatible with Gladys