Integrar tiras LED Magic Home en Gladys a través de MQTT

Hola,
Primero, quiero aclarar que tengo un perfil técnico bastante avanzado en muchos ámbitos, pero no soy desarrollador. Sin embargo, me he convertido en un vibe coder en los últimos meses. A algunos les gusta este enfoque, a otros no, pero cuando está muy bien organizado y se ha aprendido a usar correctamente las inteligencias artificiales, se pueden crear cosas realmente agradables. Pero quería decíroslo porque lo que voy a publicar a continuación ha sido realizado por mí, pero sobre todo por Claude, mi compañero de Anthropic.

Recién llegado a Gladys, haciendo algunos pequeños sacrificios, he podido reconectar todo con Z-Wave, Zigbee y Tuya. Lo único importante que falta es mi tira de LEDs Magic Home. He mirado para convertirlo a Zigbee, pero es un poco demasiado complejo, especialmente porque tengo módulos que están en techos falsos, etc.

Cuando descubrí lo que se podía hacer con MQTT, se me ocurrieron algunas ideas. Con algunas investigaciones, encontré este GitHub: GitHub - CasperVerswijvelt/magic-home-rest: Simple REST API to control magic-home lights on the same network · GitHub

En resumen, tenía todo lo necesario para construir algo agradable. Lo hice en dos horas y acabo de empezar a usarlo. Por lo tanto, aún no sé si es estable a largo plazo.

Para información, las IPs locales en los fragmentos de código no son mis verdaderas IPs.

Integrar tiras LED Magic Home en Gladys a través de MQTT

El problema

Las tiras LED Magic Home (también vendidas bajo las marcas Arilux, ZJ-WFMN-A/B/C, etc.) utilizan el protocolo propietario flux_led en WiFi local. No existe una integración nativa en Gladys para estos controladores.

Las soluciones habituales:

  • Reemplazar los controladores por Zigbee: posible, pero implica soldadura si los conectores no son compatibles (a menudo es el caso con los modelos RGBWW de 5 pines) y es un lío cuando los controladores están en techos falsos.

  • Flashear con Tasmota: no siempre posible según el chipset.

La solución: un puente MQTT personalizado

Creamos un pequeño puente Node.js que actúa como enlace entre Gladys (a través de MQTT) y los controladores Magic Home (a través del protocolo flux_led local).

Arquitectura

Gladys UI → MQTT (Mosquitto) → magic-home-bridge → WiFi local → Controlador Magic Home → Tira LED
                                      ↓
                               estado de retorno → MQTT → Gladys

Lo que necesitas

  • Gladys con la integración MQTT configurada y conectada a un broker Mosquitto.

  • Los controladores Magic Home en la misma red local que el servidor Gladys.

  • Docker para ejecutar el puente.

  • Las direcciones IP de tus controladores Magic Home (fíjalas en tu DHCP).

Paso 1: Probar la conectividad

Primero, verifica que los controladores respondan. Usamos magic-home-rest para probar:

# Ejecutar el contenedor de prueba
docker run -d --name magic-home-rest --network host \
  -e PORT=8888 \
  casperverswijvelt/magic-home-rest:latest

# Probar un controlador (reemplaza con tu IP)
curl -X POST http://localhost:8888/api/power/ \
  -H "Content-Type: application/json" \
  -d '{"address": "192.168.1.100", "power": true}'

Si obtienes OK, está bien. La tira debería encenderse.

Prueba también el color:

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

Nota: el escaneo automático (scan UDP) no siempre funciona según tu red. Por eso usamos las IP directamente.

Paso 2: Crear los dispositivos MQTT en Gladys

En Gladys, ve a Integrations → MQTT y crea un dispositivo por tira LED.

Para cada dispositivo, crea 3 funcionalidades:

Nombre Categoría Tipo ¿Sensor? Min Max
Encender Luz Binario (on/off) No 0 1
Brillo Luz Brillo No 0 100
Color Luz Color No 0 0

Usa una convención de nombres coherente para los identificadores externos:

  • Dispositivo: mqtt:cocina:tiraled-isla

  • Funcionalidades: mqtt:cocina:tiraled-isla:power, mqtt:cocina:tiraled-isla:brightness, mqtt:cocina:tiraled-isla:color

Paso 3: Desplegar el puente

Crear los archivos

Crea una carpeta para el puente (ej: /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");

// ============================================================
// CONFIGURACIÓN — Adapte esta sección a su instalación
// ============================================================

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

// Liste sus controladores aquí:
// - extId: el identificador externo del dispositivo en Gladys
// - ip: la dirección IP fija del controlador Magic Home
// - name: un nombre legible para los registros
const DEVICES = [
  { extId: "mqtt:cuisine:bdled-ilot", ip: "192.168.1.100", name: "Ilot cocina" },
  { extId: "mqtt:chambre:bdled-lit",  ip: "192.168.1.101", name: "Cama" },
  // Agregue los suyos...
];

// ============================================================
// No modifique nada a continuación a menos que sepa lo que hace
// ============================================================

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

// --- Gestión de comandos ---

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

// --- Interrogación periódica ---

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

// --- Conexión MQTT ---

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

client.on("connect", () => {
  console.log("[MQTT] Conectado 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] Suscrito a " + (DEVICES.length * 3) + " temas");

  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] Reconectando..."));

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 + " dispositivos, interrogación cada " + (POLL_INTERVAL / 1000) + "s");

Instalar las dependencias

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

Ejecutar el puente

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

Verificar los registros

docker logs magic-home-bridge

Debería ver:

[BRIDGE] Magic Home MQTT Bridge v1.0
[BRIDGE] 2 dispositivos, interrogación cada 30s
[MQTT] Conectado a mqtt://localhost:1883
[MQTT] Suscrito a 6 temas
[POLL] Ilot cocina: power=1 color=#ff6e54 brightness=100%
[POLL] Cama: power=0 color=#000000 brightness=0%

¿Cómo funciona?

Flujo de comando (Gladys → LED)

  1. Haga clic en « Encender » en el panel de control de Gladys

  2. Gladys publica 1 en el tema MQTT gladys/device/mqtt:cuisine:bdled-ilot/feature/mqtt:cuisine:bdled-ilot:power/state

  3. El puente recibe el mensaje, llama a Control.turnOn() en la IP del controlador

  4. El controlador enciende la tira LED a través de WiFi local

  5. El puente publica el estado confirmado en gladys/master/device/.../state

  6. Gladys actualiza la interfaz

Flujo de estado (LED → Gladys)

Cada 30 segundos, el puente consulta cada controlador a través de TCP y publica el estado (potencia, color, brillo) hacia Gladys. Esto permite:

  • Sincronizar el estado si alguien usa la aplicación Magic Home

  • Detectar controladores apagados/desconectados

Gestión de color + brillo

Los controladores Magic Home no tienen un canal de brillo separado — todo pasa por los valores RGB. El puente lo maneja:

  • Gladys envía el color como un entero (ej: 16711680 = rojo puro)

  • Gladys envía el brillo como un porcentaje (0-100)

  • El puente multiplica los componentes RGB por el porcentaje de brillo antes de enviar al controlador

Limitaciones conocidas

  • Sin warm white / cold white: el puente solo maneja RGB por ahora. Si sus tiras son RGBWW (5 canales), los canales blanco cálido/frio no se controlan

  • Brillo aproximado: como el brillo se simula escalando el RGB, la precisión no es perfecta al volver (interrogación)

  • Sin descubrimiento automático: debe conocer las IP de sus controladores y fijarlas en el DHCP

  • Solo un controlador a la vez: los comandos se envían secuencialmente (no hay problema en la práctica, es instantáneo)

Requisitos

  • Gladys con integración MQTT configurada

  • Mosquitto (u otro broker MQTT)

  • Docker con acceso a la red host

  • Controladores Magic Home con IP fija en la misma LAN

Probado con éxito en un NAS Synology DS1520+ (Docker) con Gladys v4 y 5 tiras LED RGBWW.

¡Super tutorial @David-Digitis :ok_hand:
Me he permitido cambiar la categoría a … Tutoriales :wink:

Gracias, lo has hecho bien :wink: