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)
-
Vous cliquez sur « Allumer » dans le dashboard Gladys
-
Gladys publie 1 sur le topic MQTT gladys/device/mqtt:cuisine:bdled-ilot/feature/mqtt:cuisine:bdled-ilot:power/state
-
Le bridge recoit le message, appelle Control.turnOn() sur l’IP du controleur
-
Le controleur allume le bandeau LED via WiFi local
-
Le bridge publie l’etat confirme sur gladys/master/device/.../state
-
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 :
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.