Tutoriales

Migración a Paho MQTT 2.0: soluciona los errores de CallbackAPIVersion

Pasa de paho-mqtt 1.x a 2.x: corrige el ValueError de CallbackAPIVersion, adapta on_connect y demás callbacks y maneja los nuevos objetos ReasonCode.

Actualizado · 8 min de lectura

Migración a Paho MQTT 2.0: soluciona los errores de CallbackAPIVersion

paho-mqtt 2.0 cambió la forma de crear un cliente y lo que reciben tus callbacks. Si tu script dejó de funcionar tras un pip install --upgrade, la solución casi siempre es la misma: pasa mqtt.CallbackAPIVersion.VERSION2 como primer argumento de mqtt.Client() y actualiza las firmas de los callbacks. Esta guía muestra los errores exactos, qué cambió y un ejemplo completo de antes y después.

Los errores que probablemente estás viendo

ErrorCausaSolución
ValueError: Unsupported callback API version: version 2.0 added a callback_api_version, see docs/migrations.rst for detailsSe pasó el client ID como primer argumento posicional: mqtt.Client("my-id")mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="my-id")
DeprecationWarning: Callback API version 1 is deprecated, update to latest versionmqtt.Client() sin versión, o con VERSION1Pasa VERSION2 y actualiza los callbacks
TypeError: on_connect() takes 4 positional arguments but 5 were givenVERSION2 con un on_connect(client, userdata, flags, rc) al estilo 1.xAñade el parámetro properties (ver abajo)
TypeError: on_disconnect() takes 3 positional arguments but 5 were givenLo mismo con on_disconnect(client, userdata, rc)Nueva firma con disconnect_flags, reason_code, properties
AttributeError: module 'paho.mqtt.client' has no attribute 'CallbackAPIVersion'Tu código es para 2.x pero el entorno sigue teniendo 1.xpip install "paho-mqtt>=2.0" y comprueba qué intérprete ejecuta el script

Comprueba primero la versión instalada; los entornos virtuales y el Python del sistema a menudo no coinciden:

¿Qué paho-mqtt está instalado?
python -c "import paho.mqtt; print(paho.mqtt.__version__)"
pip show paho-mqtt

Qué cambió en 2.0

La versión 1.x tenía un único estilo de callbacks cuyos argumentos dependían de la versión del protocolo: los callbacks de MQTT 3.1.1 recibían un entero rc y los de MQTT 5 recibían reason_code y properties. Era fácil escribir código que funcionaba con un protocolo y fallaba con el otro.

2.0 introduce las versiones de la API de callbacks. Con CallbackAPIVersion.VERSION2, cada callback tiene la misma firma en MQTT 3.1.1 y MQTT 5, los reason codes son siempre objetos ReasonCode y las properties se pasan siempre (como None cuando el protocolo no tiene). El constructor queda así:

python
mqtt.Client(
    callback_api_version,          # nuevo, primer argumento posicional
    client_id="",
    clean_session=None,
    userdata=None,
    protocol=mqtt.MQTTv311,
    transport="tcp",
    reconnect_on_failure=True,
    manual_ack=False,
)

Otras cosas que conviene saber: 2.x requiere Python 3.7 o superior, y los helpers publish.single() / subscribe.simple() gestionan la versión por ti, así que el código que solo los usa suele seguir funcionando.

Firmas de callbacks: 1.x vs VERSION2

Callback1.x (MQTT 3.1.1)2.x con VERSION2
on_connect(client, userdata, flags, rc)(client, userdata, flags, reason_code, properties)
on_disconnect(client, userdata, rc)(client, userdata, disconnect_flags, reason_code, properties)
on_subscribe(client, userdata, mid, granted_qos)(client, userdata, mid, reason_code_list, properties)
on_unsubscribe(client, userdata, mid)(client, userdata, mid, reason_code_list, properties)
on_publish(client, userdata, mid)(client, userdata, mid, reason_code, properties)
on_message(client, userdata, message)sin cambios
on_log(client, userdata, level, buf)sin cambios

En la práctica importan tres detalles:

  • flags ya no es un dict. En on_connect es un objeto ConnectFlags: usa flags.session_present en lugar de flags["session present"].
  • disconnect_flags.is_disconnect_packet_from_server te dice si el broker envió un DISCONNECT (MQTT 5) o si la propia librería detectó la pérdida.
  • on_subscribe recibe reason codes, no enteros de QoS. Para una suscripción concedida con QoS 1, el value del reason code es 1; una suscripción rechazada tiene un valor de 0x80 o superior, así que rc.is_failure es verdadero.

Los reason codes ahora son objetos

Con VERSION2, reason_code es un ReasonCode. Se compara como igual a un entero y a su nombre, se imprime como su nombre y tiene la propiedad is_failure:

python
def on_connect(client, userdata, flags, reason_code, properties):
    if reason_code.is_failure:
        print(f"conexión fallida: {reason_code} ({reason_code.value})")
        return
    print("conectado, sesión presente:", flags.session_present)

reason_code == 0 y reason_code == "Success" siguen funcionando para el caso de éxito, así que la comprobación más habitual sobrevive a la actualización sin cambios.

Un ejemplo completo de antes y después

Este es código típico de 1.x; en 2.x falla con el ValueError de arriba:

client_v1.py — paho-mqtt 1.x
import paho.mqtt.client as mqtt

def on_connect(client, userdata, flags, rc):
    if rc == 0:
        client.subscribe("testmqtt/paho2/#", qos=1)
    else:
        print("connect failed, rc =", rc)

def on_disconnect(client, userdata, rc):
    print("disconnected, rc =", rc)

def on_message(client, userdata, msg):
    print(msg.topic, msg.payload.decode())

client = mqtt.Client("testmqtt-paho2-demo")
client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883, 60)
client.loop_forever()

El mismo cliente migrado a VERSION2:

client_v2.py — paho-mqtt 2.x
import os
import paho.mqtt.client as mqtt

def on_connect(client, userdata, flags, reason_code, properties):
    if reason_code.is_failure:
        print(f"conexión fallida: {reason_code}")
        return
    print("conectado, sesión presente:", flags.session_present)
    client.subscribe("testmqtt/paho2/#", qos=1)

def on_subscribe(client, userdata, mid, reason_code_list, properties):
    for rc in reason_code_list:
        print("suscripción:", "rechazada" if rc.is_failure else f"QoS concedido {rc.value}")

def on_disconnect(client, userdata, disconnect_flags, reason_code, properties):
    print(f"desconectado: {reason_code} (desde el servidor: {disconnect_flags.is_disconnect_packet_from_server})")

def on_message(client, userdata, msg):
    print(msg.topic, msg.payload.decode())

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="testmqtt-paho2-demo")
if os.environ.get("MQTT_USER"):
    client.username_pw_set(os.environ["MQTT_USER"], os.environ.get("MQTT_PASS"))
client.on_connect = on_connect
client.on_subscribe = on_subscribe
client.on_disconnect = on_disconnect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883, keepalive=60)
client.loop_forever()

Los mismos callbacks funcionan sin cambios con protocol=mqtt.MQTTv5: esa es la principal ventaja de VERSION2. Solo cambian los argumentos del constructor: MQTT 5 no acepta clean_session (lanza ValueError: Clean session is not used for MQTT 5.0); usa clean_start y la expiración de sesión en connect(). La guía de migración de MQTT 3.1.1 a MQTT 5 cubre esas opciones.

Checklist de migración

  1. Fija la versión a la que migras, por ejemplo paho-mqtt>=2.1,<3 en requirements.txt, para que la próxima versión mayor no te sorprenda.
  2. Busca mqtt.Client( en el código y pasa primero mqtt.CallbackAPIVersion.VERSION2, con client_id= como argumento con nombre.
  3. Actualiza todos los callbacks a la nueva firma, incluidos los asignados en otros módulos o librerías.
  4. Sustituye las comparaciones de enteros sobre rc por reason_code.is_failure o comparaciones por nombre.
  5. Sustituye flags["session present"] por flags.session_present.
  6. Ejecuta con los warnings visibles (python -W error::DeprecationWarning app.py) para detectar cualquier cliente que siga en VERSION1.

Si de todos modos vas a reescribir mucho código, plantéate si te conviene más un cliente asíncrono: aiomqtt para asyncio está construido sobre paho-mqtt 2.x. Para otras excepciones de paho, consulta errores de paho-mqtt en Python.

Prueba el cliente migrado

Ejecuta client_v2.py, abre el cliente MQTT online en tu navegador, conéctate a broker.connectmq.com y publica en testmqtt/paho2/hello. Tu script debería imprimir el topic y el payload. Para probar los caminos de error, conéctate con una contraseña incorrecta a tu propio broker y comprueba que on_connect imprime un motivo legible como Bad user name or password en lugar de un número suelto.

Publica un mensaje de prueba para tu cliente migrado

La migración a paho-mqtt 2.0 es sobre todo mecánica: un nuevo argumento en el constructor, cinco firmas de callbacks y unas cuantas comprobaciones de enteros que pasan a ser comprobaciones de reason codes. Hazla una vez con VERSION2, fija la versión mayor y tus callbacks se comportarán igual en MQTT 3.1.1 y MQTT 5. Para un recorrido completo por la librería, consulta la guía de MQTT con Python.

Preguntas frecuentes

¿Qué significa 'Unsupported callback API version' en paho-mqtt?

Desde paho-mqtt 2.0, el primer argumento de mqtt.Client() es la versión de la API de callbacks. El código escrito para 1.x que pasa el client ID como primer argumento posicional lanza ahora este ValueError. Pasa primero mqtt.CallbackAPIVersion.VERSION2 y el client ID como client_id=.

¿Puedo mantener mis callbacks antiguos con paho-mqtt 2.x?

Sí, de forma temporal. Pasa mqtt.CallbackAPIVersion.VERSION1 y las firmas de callbacks de 1.x siguen funcionando, pero la librería emite un DeprecationWarning y está previsto eliminar la versión 1 en una futura versión mayor. Migra a VERSION2 en cuanto puedas.

¿Por qué mi comprobación 'if rc == 5' dejó de funcionar tras actualizar?

Con VERSION2, on_connect recibe un objeto ReasonCode incluso en MQTT 3.1.1, y los códigos de retorno de 3.1.1 se traducen a reason codes de MQTT 5. 'Not authorized' es 135 (0x87), no 5. Usa reason_code.is_failure o compara con el nombre, por ejemplo reason_code == 'Not authorized'.