Tutoriels

Migrer vers Paho MQTT 2.0 : corriger les erreurs CallbackAPIVersion

Passez de paho-mqtt 1.x à 2.x : corrigez le ValueError CallbackAPIVersion, adaptez on_connect et les autres callbacks, gérez les objets ReasonCode.

Mis à jour · 8 min de lecture

Migrer vers Paho MQTT 2.0 : corriger les erreurs CallbackAPIVersion

paho-mqtt 2.0 a changé la façon de créer un client et ce que reçoivent vos callbacks. Si votre script ne fonctionne plus après un pip install --upgrade, la correction est presque toujours la même : passez mqtt.CallbackAPIVersion.VERSION2 en premier argument de mqtt.Client() et mettez à jour les signatures des callbacks. Ce guide présente les erreurs exactes, ce qui a changé et un exemple complet avant/après.

Les erreurs que vous voyez probablement

ErreurCauseCorrection
ValueError: Unsupported callback API version: version 2.0 added a callback_api_version, see docs/migrations.rst for detailsLe client ID a été passé en premier argument positionnel : 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() sans version, ou avec VERSION1Passez VERSION2 et mettez à jour les callbacks
TypeError: on_connect() takes 4 positional arguments but 5 were givenVERSION2 avec un on_connect(client, userdata, flags, rc) façon 1.xAjoutez le paramètre properties (voir plus bas)
TypeError: on_disconnect() takes 3 positional arguments but 5 were givenIdem pour on_disconnect(client, userdata, rc)Nouvelle signature avec disconnect_flags, reason_code, properties
AttributeError: module 'paho.mqtt.client' has no attribute 'CallbackAPIVersion'Votre code vise la 2.x mais l'environnement a encore la 1.xpip install "paho-mqtt>=2.0" et vérifiez quel interpréteur exécute le script

Vérifiez d'abord la version installée — environnements virtuels et Python système divergent souvent :

Quel paho-mqtt est installé ?
python -c "import paho.mqtt; print(paho.mqtt.__version__)"
pip show paho-mqtt

Ce qui a changé dans la 2.0

La version 1.x avait un seul style de callbacks dont les arguments dépendaient de la version du protocole : les callbacks MQTT 3.1.1 recevaient un entier rc, ceux de MQTT 5 recevaient reason_code et properties. Il était donc facile d'écrire du code qui marchait avec un protocole et plantait avec l'autre.

La 2.0 introduit les versions de l'API de callbacks. Avec CallbackAPIVersion.VERSION2, chaque callback a la même signature en MQTT 3.1.1 et MQTT 5, les reason codes sont toujours des objets ReasonCode et les properties sont toujours passées (None quand le protocole n'en a pas). Le constructeur ressemble désormais à ceci :

python
mqtt.Client(
    callback_api_version,          # nouveau, premier argument positionnel
    client_id="",
    clean_session=None,
    userdata=None,
    protocol=mqtt.MQTTv311,
    transport="tcp",
    reconnect_on_failure=True,
    manual_ack=False,
)

À savoir également : la 2.x exige Python 3.7 ou plus récent, et les helpers publish.single() / subscribe.simple() gèrent la version pour vous ; le code qui n'utilise qu'eux continue donc généralement de fonctionner.

Signatures des callbacks : 1.x vs VERSION2

Callback1.x (MQTT 3.1.1)2.x avec 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)inchangé
on_log(client, userdata, level, buf)inchangé

En pratique, trois détails comptent :

  • flags n'est plus un dict. Dans on_connect, c'est un objet ConnectFlags : utilisez flags.session_present au lieu de flags["session present"].
  • disconnect_flags.is_disconnect_packet_from_server indique si le broker a envoyé un DISCONNECT (MQTT 5) ou si la bibliothèque a détecté elle-même la perte de connexion.
  • on_subscribe reçoit des reason codes, pas des entiers de QoS. Pour un abonnement accordé en QoS 1, la value du reason code vaut 1 ; un abonnement refusé a une valeur de 0x80 ou plus, donc rc.is_failure est vrai.

Les reason codes sont désormais des objets

Avec VERSION2, reason_code est un ReasonCode. Il est égal à un entier comme à son nom, s'affiche sous forme de nom et possède la propriété is_failure :

python
def on_connect(client, userdata, flags, reason_code, properties):
    if reason_code.is_failure:
        print(f"échec de connexion : {reason_code} ({reason_code.value})")
        return
    print("connecté, session présente :", flags.session_present)

reason_code == 0 et reason_code == "Success" fonctionnent toujours pour le cas de succès : le test le plus courant survit donc à la mise à jour sans modification.

Un exemple complet avant/après

Voici du code 1.x typique — il échoue en 2.x avec le ValueError ci-dessus :

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()

Le même client migré vers 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"échec de connexion : {reason_code}")
        return
    print("connecté, session présente :", 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("abonnement :", "refusé" if rc.is_failure else f"QoS {rc.value} accordé")

def on_disconnect(client, userdata, disconnect_flags, reason_code, properties):
    print(f"déconnecté : {reason_code} (par le serveur : {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()

Les mêmes callbacks fonctionnent sans changement avec protocol=mqtt.MQTTv5 — c'est le principal avantage de VERSION2. Seuls les arguments du constructeur diffèrent : MQTT 5 n'accepte pas clean_session (il lève ValueError: Clean session is not used for MQTT 5.0) ; utilisez plutôt clean_start et l'expiration de session dans connect(). Le guide de migration de MQTT 3.1.1 vers MQTT 5 détaille ces options.

Checklist de migration

  1. Épinglez la version cible, par exemple paho-mqtt>=2.1,<3 dans requirements.txt, pour que la prochaine version majeure ne vous prenne pas par surprise.
  2. Cherchez mqtt.Client( dans le code et passez d'abord mqtt.CallbackAPIVersion.VERSION2, avec client_id= en argument nommé.
  3. Mettez à jour chaque callback vers la nouvelle signature — y compris ceux assignés dans d'autres modules ou bibliothèques.
  4. Remplacez les comparaisons d'entiers sur rc par reason_code.is_failure ou des comparaisons de noms.
  5. Remplacez flags["session present"] par flags.session_present.
  6. Exécutez avec les warnings visibles (python -W error::DeprecationWarning app.py) pour repérer tout client resté en VERSION1.

Si vous réécrivez beaucoup de code de toute façon, demandez-vous si un client asynchrone ne serait pas plus adapté — aiomqtt pour asyncio repose sur paho-mqtt 2.x. Pour les autres exceptions de paho, consultez les erreurs paho-mqtt en Python.

Tester le client migré

Lancez client_v2.py, ouvrez le client MQTT en ligne dans votre navigateur, connectez-vous à broker.connectmq.com et publiez sur testmqtt/paho2/hello. Votre script doit afficher le topic et le payload. Pour tester les cas d'erreur, connectez-vous à votre propre broker avec un mauvais mot de passe et vérifiez que on_connect affiche un motif lisible comme Bad user name or password plutôt qu'un simple nombre.

Publier un message de test vers le client migré

La migration vers paho-mqtt 2.0 est surtout mécanique : un nouvel argument de constructeur, cinq signatures de callbacks et quelques tests d'entiers qui deviennent des tests de reason codes. Faites-la une fois avec VERSION2, épinglez la version majeure, et vos callbacks se comporteront de la même façon en MQTT 3.1.1 et en MQTT 5. Pour un tour complet de la bibliothèque, consultez le guide MQTT avec Python.

Questions fréquentes

Que signifie 'Unsupported callback API version' dans paho-mqtt ?

Depuis paho-mqtt 2.0, le premier argument de mqtt.Client() est la version de l'API de callbacks. Le code écrit pour la 1.x qui passe le client ID en premier argument positionnel lève désormais ce ValueError. Passez d'abord mqtt.CallbackAPIVersion.VERSION2, puis le client ID via client_id=.

Puis-je garder mes anciens callbacks avec paho-mqtt 2.x ?

Oui, temporairement. Avec mqtt.CallbackAPIVersion.VERSION1, les signatures de callbacks de la 1.x continuent de fonctionner, mais la bibliothèque émet un DeprecationWarning et la version 1 doit être supprimée dans une future version majeure. Migrez vers VERSION2 dès que possible.

Pourquoi mon test 'if rc == 5' ne fonctionne-t-il plus après la mise à jour ?

Avec VERSION2, on_connect reçoit un objet ReasonCode même en MQTT 3.1.1, et les codes de retour 3.1.1 sont convertis en reason codes MQTT 5. 'Not authorized' vaut 135 (0x87), pas 5. Utilisez reason_code.is_failure ou comparez avec le nom, par exemple reason_code == 'Not authorized'.