Zigbee2MQTT sin dispositivos tras reiniciar Proxmox: causa y solución definitiva

Dongle Sonoff Zigbee 3.0 USB conectado a un servidor Proxmox
1 Vistas
0 Valoración

Zigbee2MQTT sin dispositivos tras reiniciar Proxmox: causa y solución definitiva

El fin de semana pasado cambié la batería de mi SAI Riello. Nada del otro mundo: apagar los dispositivos conectados, sustituir la batería, volver a encender. Lo que no esperaba es que al reiniciar Proxmox, todos mis dispositivos Zigbee desaparecieran de Home Assistant sin dejar rastro aparente.

Tres días después de investigar, el diagnóstico es simple: el dongle Sonoff Zigbee 3.0 USB Dongle Plus cambió de /dev/ttyUSB0 a /dev/ttyUSB2 tras el reinicio, rompiendo el passthrough USB al LXC donde corre Zigbee2MQTT. En este post documento el proceso de diagnóstico completo y la solución definitiva para que no vuelva a pasar.

El contexto: Zigbee2MQTT en un LXC de Proxmox

Mi stack domótico corre así:

  • Proxmox como hipervisor en un servidor dedicado
  • LXC 109 con Zigbee2MQTT (Debian)
  • LXC con Mosquitto (broker MQTT)
  • VM con Home Assistant OS
  • Sonoff Zigbee 3.0 USB Dongle Plus pasado al LXC 109 mediante passthrough USB

El passthrough USB en LXC de Proxmox funciona montando el dispositivo del host dentro del contenedor mediante entradas en el fichero /etc/pve/lxc/<ID>.conf. Llevaba funcionando sin problemas durante años.

Los síntomas

Al arrancar Home Assistant tras el reinicio de Proxmox, las notificaciones empezaron a llegar: dispositivos Zigbee no disponibles. Sensores, enchufes inteligentes, controladores LED... todos offline.

Zigbee2MQTT, en cambio, no mostraba ningún error obvio en la interfaz — simplemente no arrancaba correctamente y se reiniciaba en bucle.

Diagnóstico paso a paso

1. Revisar los logs de Zigbee2MQTT

Lo primero es siempre mirar los logs. Dentro del LXC:

journalctl -u zigbee2mqtt -n 100 --no-pager

El error era claro y repetitivo:

error: z2m: Error: No such file or directory, cannot open
/dev/serial/by-id/usb-ITead_Sonoff_Zigbee_3.0_USB_Dongle_Plus_bc58aeb0acdbed11b349f12d62c613ac-if00-port0

Z2M no encontraba el dongle. El dispositivo configurado en configuration.yaml no existía dentro del LXC.

2. Verificar el dispositivo dentro del LXC

ls -la /dev/serial/by-id/

El symlink existía dentro del LXC:

lrwxrwxrwx 1 root root 13 Mar 15 09:48
usb-ITead_Sonoff_Zigbee_3.0_USB_Dongle_Plus_[...]-if00-port0 -> ../../ttyUSB2

Pero apuntaba a ttyUSB2, que no existía en el LXC. Y los permisos del ttyUSB0 que sí existía eran ---------- — cero permisos para todos.

3. Verificar en el host Proxmox

En la shell de PVE:

journalctl -k --since "2026-03-01" | grep -i "sonoff\|cp210x\|10c4" | tail -10

Resultado revelador:

Mar 15 09:48:08 pve kernel: cp210x converter now attached to ttyUSB2

El Sonoff se había asignado a ttyUSB2 al arrancar, probablemente porque otro dispositivo USB (el RFXtrx433E, que usa el driver ftdi_sio) se inicializó antes y ocupó ttyUSB0.

4. La causa raíz

El número ttyUSBx lo asigna el kernel dinámicamente según el orden en que detecta los dispositivos USB al arrancar. No es fijo. Antes del reinicio el Sonoff era ttyUSB0; después pasó a ser ttyUSB2.

La configuración del LXC montaba ttyUSB0 y ttyUSB1 pero no ttyUSB2, así que el dongle era visible en /dev/serial/by-id/ (porque ese directorio se montaba completo), pero el symlink apuntaba a un ttyUSB2 que no existía dentro del contenedor.

La respuesta estaba en casa

Lo más frustrante del diagnóstico es que ya había resuelto exactamente este problema años atrás. Cuando monté Zigbee con OpenHAB en una Raspberry Pi, el coordinador CC2531 y el RFXtrx433E tenían el mismo problema de nombres dinámicos. La solución fue idéntica: una regla udev en /etc/udev/rules.d/10-local.rules con SYMLINK+="ttyUSB-ZIGBEE" para fijar el nombre del dispositivo.

Lo documenté en detalle en el post Zigbee con OpenHAB y CC2531. La técnica es exactamente la misma, solo cambia el contexto: ahí era una Raspberry con OpenHAB, aquí es un host Proxmox con un LXC.

El fallo fue mío: al migrar de la Raspberry a Proxmox apliqué el passthrough USB pero no trasladé la regla udev. Confiaba en que el mount de /dev/serial/by-id era suficiente, y durante años lo fue — hasta que un reinicio en frío cambió el orden de detección del kernel.

La solución: reglas udev con nombre fijo

La solución correcta es asignar un nombre permanente al dongle mediante una regla udev en el host Proxmox, independientemente del número ttyUSBx que le asigne el kernel en cada arranque.

Paso 1: Crear la regla udev en PVE

nano /etc/udev/rules.d/99-usb-serial.rules
# Sonoff Zigbee 3.0 USB Dongle Plus
SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", ATTRS{serial}=="bc58aeb0acdbed11b349f12d62c613ac", SYMLINK+="ttyUSB-Sonoff"

Los valores idVendor, idProduct y serial se obtienen con:

udevadm info -a -n /dev/ttyUSB2 | grep -E 'idVendor|idProduct|"{serial}"' | head -5

O directamente desde el bus USB si el dispositivo no está disponible como tty:

udevadm info /sys/bus/usb/devices/1-3.3 | grep -E 'SERIAL|VENDOR|PRODUCT'

Aplicar las reglas:

udevadm control --reload-rules && udevadm trigger

Verificar que el symlink se ha creado:

ls -la /dev/ttyUSB-*
# lrwxrwxrwx 1 root root 7 Mar 17 07:42 /dev/ttyUSB-Sonoff -> ttyUSB2

Paso 2: Actualizar la configuración del LXC

Editar /etc/pve/lxc/109.conf y reemplazar los mounts de ttyUSBx por el nombre fijo:

Antes:

lxc.mount.entry: /dev/serial/by-id  dev/serial/by-id  none bind,optional,create=dir
lxc.mount.entry: /dev/ttyUSB0       dev/ttyUSB0       none bind,optional,create=file
lxc.mount.entry: /dev/ttyUSB1       dev/ttyUSB1       none bind,optional,create=file
lxc.mount.entry: /dev/ttyACM0       dev/ttyACM0       none bind,optional,create=file
lxc.mount.entry: /dev/ttyACM1       dev/ttyACM1       none bind,optional,create=file

Después:

lxc.mount.entry: /dev/ttyUSB-Sonoff  dev/ttyUSB-Sonoff  none bind,optional,create=file

Reiniciar el LXC:

pct reboot 109

Paso 3: Actualizar configuration.yaml de Zigbee2MQTT

Dentro del LXC, editar /opt/zigbee2mqtt/data/configuration.yaml:

Antes:

serial:
  port: >-
    /dev/serial/by-id/usb-ITead_Sonoff_Zigbee_3.0_USB_Dongle_Plus_bc58aeb0acdbed11b349f12d62c613ac-if00-port0

Después:

serial:
  port: /dev/ttyUSB-Sonoff

Reiniciar Zigbee2MQTT:

systemctl restart zigbee2mqtt
journalctl -u zigbee2mqtt -f

Si todo ha ido bien, verás los dispositivos cargando en los logs y el Zigbee2MQTT started! final.

¿Por qué no había pasado antes?

Llevaba años con esta configuración sin problemas porque el servidor raramente se reinicia. Los reinicios controlados que había hecho antes no cambiaban el orden de detección USB porque los dispositivos siempre estaban conectados y el kernel los detectaba en el mismo orden.

El cambio de batería del SAI implicó apagar completamente el servidor, y en ese arranque en frío el kernel detectó los dispositivos en un orden ligeramente diferente, asignando ttyUSB2 al Sonoff en lugar de ttyUSB0.

Conclusión

El passthrough USB en LXC de Proxmox es frágil si se basa en nombres dinámicos como ttyUSBx. La solución correcta es siempre usar nombres fijos mediante reglas udev en el host, montando esos nombres fijos en el LXC. Así el setup es resiliente a reinicios, reconexiones y cambios en el orden de detección USB del kernel.

Si tienes más de un dispositivo USB serial en Proxmox (en mi caso también tengo un RFXtrx433E pasado por USB a la VM de Home Assistant), te recomiendo crear una regla udev para cada uno y documentar los idVendor, idProduct y serial de cada dispositivo. Ese fichero /etc/udev/rules.d/99-usb-serial.rules es el seguro de vida de tu infraestructura domótica.

¿Te ha sido útil?

Ayúdame a mejorar con tu puntuación y comentarios.

0.0 (0 votos)
Jaume Ferré

Jaume Ferré

Soy un entusiasta de las nuevas tecnologías, apasionado por explorar su potencial innovador. Colecciono CDs en formato físico y disfruto creando mezclas musicales. Además, la fotografía es otra de mis pasiones, capturando momentos y expresiones con cada disparo. ¡Gracias por leerme!

Comentarios

Los comentarios están gestionados por GitHub Discussions. Necesitas una cuenta de GitHub para participar. ¡Es gratis y rápido!