Llevaba tiempo pensando en sincronizar los LEDs del perfil de la terraza con la música. No como una discoteca, sino como un ambiente: una cena de verano, música sonando por el altavoz Bluetooth, y las luces pulsando suavemente al ritmo. Esta semana lo monté — y de paso sobreviví a un ban de IP, un modo de recuperación de HA y un Python 3.14 con ganas de fastidiar.
Una mención especial para Claude.ai (para los amigos, Claudie), sin el cual este proyecto habría llevado el doble de tiempo. Tener un asistente que entiende el contexto del homelab, recuerda la arquitectura y ayuda a depurar en tiempo real es una ventaja enorme para este tipo de proyectos nocturnos.
El setup
Antes de entrar en materia, el contexto físico:
- Tira LED SMD 5050 RGB en el perfil interior de la terraza, controlada por un Gledopto Zigbee RGB controller integrado en Zigbee2MQTT como
light.leds_terraza - Behringer UMC404HD como interfaz de audio central en el despacho (mi equipo de sobremesa, Arch Linux)
- 1Mii ML302+ (transmisor Bluetooth) conectado a los outputs 3/4 del Behringer → altavoz BT en la terraza
- Home Assistant corriendo en una VM (HAOS, IP 192.168.1.20) sobre Proxmox en el homelab
El flujo de audio es: PC → Behringer → ML302+ → altavoz BT terraza. La música nunca toca HA. Eso es clave para entender la arquitectura.
La arquitectura
Behringer UMC404HD (En mi equipo de sobremesa)
↓ monitor de salida (PipeWire)
Script Python → API REST → HA → Z2M → Gledopto → LEDs terrazaEl script corre en mi equipo de sobremesa, captura el audio directamente del monitor de salida del Behringer via PipeWire, analiza las frecuencias en tiempo real y envía comandos a HA por la API REST local.
Por qué no WLED ni controlador con micrófono: los LEDs están en la terraza y el Behringer en el despacho. No hay cable de audio entre ellos. Capturar desde PipeWire en origen es la solución más limpia.
El problema de sounddevice con Python 3.14
El primer intento fue con sounddevice, la librería habitual para captura de audio en Python. En Arch con Python 3.14 — crash inmediato con segfault o error de sample rate. No es un bug mío, es una incompatibilidad conocida.
La solución fue usar parec (PulseAudio/PipeWire) directamente via subprocess, pasando el audio por pipe a Python:
parec --device=alsa_output.usb-BEHRINGER_UMC404HD_192k-00.HiFi__Line1__sink.monitor \
--format=s16le --rate=48000 --channels=2Esto funciona perfectamente y es más estable que sounddevice en este entorno.
Detección de beats
El primer enfoque — calcular energía de frecuencias y normalizarla contra un valor fijo — no funcionó bien. La música tiene un nivel medio bastante constante y los LEDs se quedaban en un brillo fijo sin apenas variación.
La solución real es detección de onset: comparar el nivel actual de bajos con el promedio de las últimas N muestras. Si el valor actual supera 1.3x la media, es un beat.
def detect_beat(bass_n):
bass_history.append(bass_n)
if len(bass_history) > HISTORY_SIZE:
bass_history.pop(0)
promedio = sum(bass_history) / len(bass_history)
if bass_n / promedio > BEAT_THRESHOLD:
return BEAT_BRIGHTNESS # pico → brillo máximo
else:
return max(BASE_BRIGHTNESS, (bass_n / promedio) * 0.3)Así el sistema se adapta automáticamente al volumen y al tipo de música.
Control desde Home Assistant
Para no tener el script machacando los LEDs constantemente, el sistema tiene tres estados:
- Boolean OFF: el script no toca los LEDs, HA los controla con normalidad
- Boolean ON: el script activa el modo reactivo, guardando previamente el estado de los LEDs
- Al desactivar: restaura automáticamente el color y brillo anteriores
Los parámetros de ajuste son input_number helpers en HA, que el script lee cada 2 segundos. Así se pueden tocar en tiempo real desde un dashboard sin reiniciar nada:
input_number:
music_bass_norm:
name: "Sensibilidad bajos"
min: 50
max: 500
step: 10
initial: 200
music_beat_threshold:
name: "Umbral de beat"
min: 1.0
max: 3.0
step: 0.1
initial: 1.3
# ... etcEl dashboard
Una vista dedicada en HA con estética oscura, sliders para cada parámetro, botón de activar/desactivar y botón de restablecer valores por defecto. Todo en YAML, con mushroom cards y card-mod para el estilo.

Lo que salió mal (y cómo se arregló)
Autenticación con la API de HA: durante las pruebas tuve problemas con el token de acceso — hasta que no lo regeneré correctamente desde el perfil de usuario en HA, la API rechazaba las peticiones. Tres intentos fallidos son suficientes para que HA banee la IP, y con login_attempts_threshold: 3 en la config, es muy fácil llegar ahí. Solución definitiva: borrar /config/ip_bans.yaml y cambiar ip_ban_enabled: false — el ban solo tiene sentido para IPs externas, no para dispositivos de la LAN.
Modo de recuperación de HA: un slug con guión bajo (lovelace-leds_music-setup) en configuration.yaml es inválido. HA lo rechaza y arranca en modo recuperación. Los slugs solo admiten guiones normales.
Python 3.14 + sounddevice: incompatibilidad conocida. Solución: parec via subprocess.
Estado actual y próximos pasos
El sistema funciona, pero el script aún está en fase de ajuste. La latencia Zigbee (300-500ms) es el límite real del sistema — para house music a 128bpm el efecto se nota pero la sincronización no es perfecta. Los parámetros de detección de beats necesitan más tiempo de prueba en condiciones reales (de noche, con música, en la terraza) antes de dar el proyecto por cerrado.
Cuando el script esté más pulido, lo publicaré en el repositorio con la configuración completa de HA.
El siguiente paso natural sería sustituir el Gledopto por un ESP32 con WLED y entrada de línea directa — sin pasar por HA, sin latencia Zigbee. Pero eso es para otro post.