Integrer des bandeaux LED Magic Home dans Gladys via MQTT

Hello,
Tout d’abord, je tiens à préciser que j’ai un profil technique assez poussé dans beaucoup de domaines, mais je ne suis pas développeur. Je suis par contre devenu vibe coder ces derniers mois. Certains aiment cette approche d’autres pas quoi qu’il en soit quand c’est très bien organisé et qu’on a correctement appris à se servir des intelligences artificielles argentiques, on peut sortir des choses franchement sympathiques. mais je tenais à vous le dire car ce que je vais poster après a été réalisé par moi mais surtout par Claude mon copain de chez Anthropic.

Fraichement débarqué sur Gladys, en faisant quelques petits sacrifices, j’ai pu tout reconnecter avec du Z-Wave, du Zigbee et Tuya. Le seul truc important qui manque à l’appel, c’est mes bandeaux LED Magic Home. J’ai regardé pour convertir sur du Zigbee mais c’est un peu trop complexe, surtout que j’ai des modules qui sont dans des faux plafonds, etc.

Quand j’ai découvert ce qu’on pouvait faire avec MQTT, ça m’a donné des idées. Avec quelques recherches, je suis tombé sur ce GitHub : GitHub - CasperVerswijvelt/magic-home-rest: Simple REST API to control magic-home lights on the same network · GitHub

Donc en bref j’avais tout ce qu’il fallait pour construire quelque chose de sympathique. J’ai fait ça en deux heures et je viens seulement de commencer à l’utiliser. donc je ne sais pas encore si c’est stable sur le long terme.

Pour info, les IP locales dans les extraits de code ne sont pas mes vraies IP.

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 « J'aime »

Super tuto @David-Digitis :ok_hand:
Je me suis permis de modifier la catégorie en … Tutoriels :wink:

1 « J'aime »

Merci tu as bien fait :wink:

Excellent ! Super propre :slight_smile:

Si jamais tu veux faire plus simple, et permettre à d’autres gens d’utiliser cette intégration, tu pourrais écrire un petit plugin Matterbridge (avec l’aide de l’IA, relativement facile), comme ça cet appareil deviendrait Matter, et donc compatible Gladys (mais pas que!)

Le plugin c’est juste un simple fichier JS où tu mappes tes commandes LED Magic Home à un SDK Matter, et ensuite il faut juste héberger le plugin sur un repo Git pour ensuite l’installer dans Matterbridge.

Matterbridge se lance en un clic dans Gladys :