Un simulateur d'appareils MQTT permet de tester un broker, un backend ou un dashboard face à une flotte qui n'existe pas encore. Toute l'astuce consiste à faire en sorte que chaque appareil virtuel se comporte comme un vrai : sa propre connexion et un client ID unique, un Last Will qui le marque hors ligne, une télémétrie à intervalles légèrement irréguliers, des commandes auxquelles il répond et, de temps en temps, une coupure de courant.
Ce guide construit un tel simulateur en environ 120 lignes de Python avec asyncio et aiomqtt, le pousse à 1 000 appareils et montre comment vérifier ce que voit le broker.
Ce que fait un appareil virtuel réaliste
| Comportement | Pourquoi c'est important pour les tests |
|---|---|
| Connexion propre, client ID unique | Sollicite les sessions par client, les ACL et les limites de connexions. Des ID en double s'éjectent mutuellement. |
| Démarrage échelonné | Les vraies flottes ne se connectent pas à la même milliseconde — sauf quand le courant revient après une panne. |
Statut online retenu + Last Will offline | Permet aux dashboards d'afficher la présence et vérifie que le broker déclenche les wills. |
| Intervalle de publication avec jitter | Évite les rafales parfaitement synchronisées que de vrais appareils ne produisent jamais. |
| Flotte hétérogène | Certains appareils publient toutes les 5 s, d'autres toutes les 30 s ; les payloads diffèrent. |
| Topic de commandes | Teste le chemin descendant : backend → broker → appareil → acquittement. |
| Coupures d'alimentation | L'appareil disparaît sans DISCONNECT, puis redémarre et se reconnecte avec backoff. |
Organisation des topics
Chaque appareil possède un petit sous-arbre. Un préfixe par exécution sépare les tests lancés en parallèle :
| Topic | Sens | Retenu | Contenu |
|---|---|---|---|
testmqtt/sim/<id>/status | appareil → broker | oui | online, ou offline via le Last Will |
testmqtt/sim/<id>/telemetry | appareil → broker | non | Mesure JSON |
testmqtt/sim/<id>/cmd | backend → appareil | non | Commande JSON, ex. {"interval": 2} |
testmqtt/sim/<id>/cmd/ack | appareil → backend | non | Acquittement |
Les client ID suivent le modèle sim-<run>-<index>, par exemple sim-3fa2c1-0042. Ils sont uniques, lisibles dans les logs du broker et faciles à filtrer. Le générateur de client ID et les bonnes pratiques pour les client ID expliquent pourquoi ces propriétés comptent.
Le simulateur
Installez la dépendance avec pip install aiomqtt (version 2.x, basée sur paho-mqtt 2.x). Chaque appareil est une coroutine avec son propre aiomqtt.Client :
import asyncio, json, os, random, socket, sys, time, uuid
from collections import Counter
import aiomqtt
HOST, PORT = os.environ.get("MQTT_HOST", "broker.connectmq.com"), 1883
USER, PASS = os.environ.get("MQTT_USER"), os.environ.get("MQTT_PASS")
N_DEVICES = int(os.environ.get("DEVICES", 100))
RAMP_S = 60 # spread device boot over one minute
CRASH_PROB = 0.002 # chance of a power loss after each publish
RUN = uuid.uuid4().hex[:6]
stats = Counter()
class PowerLoss(Exception):
pass
async def commands(client, base, state):
try:
async for msg in client.messages:
try:
cmd = dict(json.loads(msg.payload))
if "interval" in cmd:
state["interval"] = max(1.0, float(cmd["interval"]))
except (ValueError, TypeError):
continue # ignore malformed commands
await client.publish(f"{base}/cmd/ack", json.dumps({"ok": True, **cmd}), qos=1)
stats["commands"] += 1
except aiomqtt.MqttError:
pass # connection lost; device() reconnects
async def device(i: int):
dev_id = f"sim-{RUN}-{i:04d}"
base = f"testmqtt/sim/{dev_id}"
rng = random.Random(i)
state = {"interval": rng.choice([5, 10, 30])}
temp, boots, attempt = rng.uniform(19, 24), 0, 0
await asyncio.sleep(rng.uniform(0, RAMP_S)) # staggered boot
while True:
try:
will = aiomqtt.Will(f"{base}/status", b"offline", qos=1, retain=True)
async with aiomqtt.Client(HOST, PORT, identifier=dev_id, username=USER,
password=PASS, keepalive=30, will=will) as client:
attempt, boots, started = 0, boots + 1, time.monotonic()
stats["online"] += 1
try:
await client.publish(f"{base}/status", b"online", qos=1, retain=True)
await client.subscribe(f"{base}/cmd", qos=1)
listener = asyncio.create_task(commands(client, base, state))
try:
while True:
temp += rng.gauss(0, 0.05)
reading = {"temp": round(temp, 2), "boots": boots,
"uptime": int(time.monotonic() - started), "fw": "1.4.2"}
await client.publish(f"{base}/telemetry", json.dumps(reading), qos=1)
stats["published"] += 1
if rng.random() < CRASH_PROB:
# power loss: drop TCP without DISCONNECT so the broker fires the will
# (aiomqtt has no public API for this, so reach the paho client)
client._client.socket().shutdown(socket.SHUT_RDWR)
await asyncio.sleep(1)
raise PowerLoss
await asyncio.sleep(state["interval"] * rng.uniform(0.9, 1.1))
finally:
listener.cancel()
finally:
stats["online"] -= 1
except PowerLoss:
stats["power_losses"] += 1
await asyncio.sleep(rng.uniform(5, 30)) # time to reboot
except aiomqtt.MqttError:
stats["conn_errors"] += 1
attempt += 1
await asyncio.sleep(min(60, 2 ** attempt) * rng.uniform(0.5, 1.0))
async def report():
while True:
await asyncio.sleep(10)
print(time.strftime("%H:%M:%S"), dict(stats), flush=True)
async def main():
print(f"run {RUN}: starting {N_DEVICES} devices under testmqtt/sim/sim-{RUN}-*")
await asyncio.gather(report(), *(device(i) for i in range(N_DEVICES)))
if __name__ == "__main__":
if sys.platform == "win32":
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
asyncio.run(main())Quelques détails rendent ce simulateur fiable :
random.Random(i)par appareil — l'appareil 42 a toujours le même intervalle, la même température de départ et le même schéma de pannes : un bug observé une fois peut être reproduit.- Statut retenu avec le Last Will correspondant — l'appareil publie
onlineaprès sa connexion ; s'il disparaît, le broker publie le will retenuoffline. Lisez Last Will and Testament dans MQTT pour savoir précisément quand les wills se déclenchent. - Reconnexion avec backoff exponentiel et jitter — sans cela, 1 000 appareils qui perdent le broker au même instant reviennent tous au même instant. Ce schéma, et la façon de l'éviter, est décrit dans les tempêtes de reconnexion MQTT.
- Windows — aiomqtt y a besoin de la boucle d'événements à sélecteur, d'où la ligne de policy en bas du fichier.
Faire tourner 1 000 appareils
ulimit -n 4096 # one socket per device, plus headroom
export MQTT_HOST=your-broker-host # your own broker instance for the full fleet
export MQTT_USER=username MQTT_PASS=password
DEVICES=1000 python simulator.pyToutes les dix secondes, le simulateur affiche une ligne comme {'online': 1000, 'published': 48213, 'power_losses': 3, ...}. Avec les intervalles par défaut (5, 10 ou 30 s par appareil), 1 000 appareils produisent de l'ordre de 100 messages par seconde : une télémétrie réaliste, pas un benchmark. Pour charger davantage, raccourcissez les intervalles ou lancez d'autres processus avec des préfixes RUN différents ; pour une pression purement liée aux connexions, voir le stress test de connexions MQTT.
Vérifier ce que voit le broker
Comptez les appareils que le broker considère actuellement en ligne — les messages de statut retenus sont livrés dès l'abonnement :
mosquitto_sub -h broker.connectmq.com -p 1883 -u "$MQTT_USER" -P "$MQTT_PASS" \
-t 'testmqtt/sim/+/status' -v -W 5 | grep -c ' online$'Regardez les wills se déclencher en temps réel en vous abonnant au même filtre dans le client MQTT en ligne et en attendant une coupure : le statut de l'appareil passe à offline, puis revient à online après son redémarrage simulé.
Testez le chemin descendant en envoyant une commande à un appareil et en attendant l'acquittement :
mosquitto_sub -h broker.connectmq.com -u "$MQTT_USER" -P "$MQTT_PASS" \
-t 'testmqtt/sim/sim-3fa2c1-0042/cmd/ack' -C 1 -v &
mosquitto_pub -h broker.connectmq.com -u "$MQTT_USER" -P "$MQTT_PASS" \
-t 'testmqtt/sim/sim-3fa2c1-0042/cmd' -q 1 -m '{"interval": 2}'Remplacez 3fa2c1 par l'identifiant d'exécution affiché par le simulateur au démarrage. Après l'acquittement, la télémétrie de cet appareil doit arriver toutes les deux secondes.
Étendre le simulateur
- Payloads réalistes — remplacez la valeur de température unique par le modèle de capteur du générateur de données de test MQTT, avec cycle quotidien, dérive et encodage binaire.
- TLS — passez
port=8883ettls_params=aiomqtt.TLSParameters()àaiomqtt.Clientpour mesurer le surcoût des handshakes TLS au démarrage. - MQTT 5 — ajoutez
protocol=aiomqtt.ProtocolVersion.V5et des propriétés de session expiry pour tester les sessions persistantes et l'expiration des messages. - Types d'appareils — lancez plusieurs fonctions coroutines (
thermostat,meter,gateway) avec des topics et des cadences différents depuis le mêmemain(). - Reconnexion massive — mettez
CRASH_PROBà zéro et redémarrez le broker pendant une exécution pour voir comment toute la flotte se rétablit.
Regardez les appareils simulés se connecter
Un simulateur transforme « on pense que ça tient la charge » en une expérience que vous pouvez relancer chaque semaine. Donnez à chaque appareil virtuel une vraie identité, faites-le tomber en panne comme le fait le matériel et vérifiez le résultat sur le broker : les mille premiers appareils réels ne seront alors que la répétition d'un test déjà réussi.
Questions fréquentes
Combien d'appareils MQTT simulés une machine peut-elle faire tourner ?
Avec asyncio, un seul processus Python fait tourner sans difficulté de quelques centaines à quelques milliers d'appareils à des cadences de télémétrie classiques, car chaque appareil est une coroutine et non un thread. Les limites pratiques sont le nombre de descripteurs de fichiers (ulimit -n), les limites de connexions du broker et le CPU consommé par le JSON et le TLS. Au-delà, utilisez plusieurs processus ou plusieurs machines.
Chaque appareil simulé doit-il avoir sa propre connexion MQTT ?
Oui, si vous voulez tester le broker comme une vraie flotte l'utilise. Une connexion par appareil sollicite la gestion des connexions, le keep-alive, les sessions, le Last Will et les ACL par client. Publier pour de nombreux appareils sur une seule connexion ne teste que le débit.
Comment simuler une coupure d'alimentation d'un appareil ?
Fermez la connexion TCP sans envoyer de DISCONNECT. Le broker considère alors la déconnexion comme inattendue et publie le Last Will de l'appareil. Une déconnexion propre, au contraire, supprime le will et ne teste donc pas votre détection des appareils hors ligne.



