Préambule
Ce projet Ze2MQTT s’intègre dans ma stratégie globale de domotisation de la recharge de mon véhicule électrique Dacia Spring.
Mon script Python s’appuie sur le package renault-api.
L’objectif est de récupérer régulièrement le statut du véhicule (batterie, autonomie, kilométrage…) pour historiser ces données, et surtout de les exploiter dans Node-RED, notamment pour estimer le temps de recharge nécessaire.
Pré-requis
Avant de commencer, assure-toi d’avoir :
✔️ Une machine virtuelle (VM) ou un nano-ordinateur (Raspberry Pi, Debian, etc.).
✔️ Un accès SSH et les droits root.
✔️ Docker installé. (article en préparation)
Etape par étape
Les dépendances système (APT)
sudo apt update sudo apt install -y python3 python3-pip
…python
J’enchaine sur les dépendances Python (PIP)
pip install renault-api renault-api[cli]
Récupération des identifiants Renault
Avant de créer le container Docker pour mon script je lance la commande suivante afin obtenir les KAMEREON_ACCOUNT_ID et VEHICLE_VIN.
Le VIN (Vehicle Identification Number) est également inscrit sur la carte grise et visible dans l’application mobile MyDacia.
renault-api --log status
Voici les réponses obtenu :
KAMEREON_ACCOUNT_ID corresponds à l’ID en face de la ma marque, dans mon cas MYDACIA
Please select a locale [en_US]: fr_FR
Do you want to save the locale to the credential store? [y/N]: y
User: ton_email
Password: ton_mot_de_passe
ID Type Vehicles
-- ------------------------------------ --------- ----------
1 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx MYDACIA 1
2 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx MYRENAULT 0
3 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx SFDC 0
Please select account [2]: 1
Do you want to save the account ID to the credential store? [y/N]: y
Vin Registration Brand Model
-- ----------------- -------------- ------- -------
1 XXXXXXXXXXXXXXXXX XX000XX DACIA SPRING
Please select vehicle [1]: 1
Do you want to save the VIN to the credential store? [y/N]: y
charge-mode: The access is forbidden
lock status: {"errors":[{"errorCode":404,"errorMessage":"There is no data for this vin and uid","errorLevel":"error","errorType":"functional"}],"error_reference":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}
res state: The access is forbidden
pressure: {"errors":[{"errorCode":404,"errorMessage":"There is no data for this vin and uid","errorLevel":"error","errorType":"functional"}],"error_reference":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}
---------------- -------------------------
Battery level 72 %
Last updated 2025-03-11 07:51:32
Range estimate 136 km
Plug state PlugState.UNPLUGGED
Charging state ChargeState.NOT_IN_CHARGE
Time remaining 165 min
Total mileage 21488.0 km
GPS Latitude 88.07982326
GPS Longitude 16.5960393
GPS last updated 2025-03-11 07:50:11
HVAC status off
---------------- -------------------------
Création du container Docker
(À venir un article dédié : “Le Dockerfile du Destin” – une image Docker unique pour tous tes bidule2MQTT 😉)
Je crée le répertoire d’installation :
mkdir -p ~/docker/ze2mqtt && cd ~/docker/ze2mqtt
Création d’un fichier docker-compose.yml :
nano docker-compose.yml
Contenu du fichier :
services:
ze2mqtt:
build: .
container_name: ze2mqtt
restart: unless-stopped
volumes:
- ./ze2mqtt.py:/app/ze2mqtt.py
- ./config.json:/app/config.json
logging:
driver: journald
environment:
- TZ=Europe/Paris # Pour que les logs soient à la bonne heure
Création d’un fichier dockerfile :
nano dockerfile
Contenu du fichier :
# Image de base légère avec Python
FROM python:3.11-slim
# Variables d’environnement par défaut (modifiable via docker run ou compose)
ENV PYTHONUNBUFFERED=1
# Dossier de travail dans le conteneur
WORKDIR /app
# Copie des fichiers dans le conteneur
COPY requirements.txt ./
# Installation des dépendances
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
libssl-dev \
libffi-dev \
python3-dev \
&& rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir -r requirements.txt
# Commande lancée automatiquement
CMD ["python", "ze2mqtt.py"]
Création d’un fichier requirements.txt :
nano requirements.txt
Contenu du fichier :
paho-mqtt aiohttp renault-api
Le script de status
Je crée le script ze2mqtt.py celui qui va actualiser le statut de mon VE
nano ze2mqtt.py
Contenu du fichier :
import asyncio
import json
import logging
import os
import signal
import sys
import aiohttp
import paho.mqtt.client as mqtt
from renault_api.renault_client import RenaultClient
# Chargement du fichier de configuration JSON
def load_config():
with open('./config.json', 'r') as f:
config = json.load(f)
return config
config = load_config()
# Configuration
LOG_LEVEL = config.get("log_level", "INFO").upper()
## Conf -> RENAULT
RENAULT_EMAIL = config["renault"]["email"]
RENAULT_PASSWORD = config["renault"]["password"]
RENAULT_LOCALE = config["renault"]["locale"]
## Conf -> MQTT
MQTT_BROKER = config["mqtt"]["broker"]
MQTT_PORT = config["mqtt"]["port"]
MQTT_USERNAME = config["mqtt"].get("username")
MQTT_PASSWORD = config["mqtt"].get("password")
MQTT_PREFIX = config["mqtt"]["prefix_topic"]
INTERVAL = config["interval"]["basic_info_period"]
# Configuration du logger
logging.basicConfig(level=getattr(logging, LOG_LEVEL, logging.INFO),
format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger("Ze2MQTT")
# Initialisation MQTT
mqtt_client = None
def init_mqtt():
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
if MQTT_USERNAME:
client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
client.on_connect = on_connect
client.on_message = lambda client, userdata, msg: asyncio.run(on_message(client, userdata, msg))
client.connect(MQTT_BROKER, MQTT_PORT, 60)
client.loop_start()
return client
# Publication MQTT
def publish(client, topic, payload, retain=False):
client.publish(topic, json.dumps(payload), retain=retain)
logger.debug(f"Publié: {topic} -> {payload}")
# Fonction appelée en cas de connexion au broker MQTT
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code == 0:
logger.info("Connecté au broker MQTT")
for vehicle_config in config["vehicle"]:
vin = vehicle_config["vin"]
topic = f"{MQTT_PREFIX}/{vin}/command"
client.subscribe(topic)
logger.info(f"Souscription au topic de commande : {topic}")
else:
logger.error(f"Erreur de connexion MQTT : {reason_code}")
# Construction du topic spécifique au VE
def build_topic(vehicle_config):
vin = vehicle_config["vin"]
name = vehicle_config.get("friendly_name", vin)
return f"{MQTT_PREFIX}/{vin}/{name}"
# Fonction appelée lors de la réception d'un message sur le topic de commande
async def on_message(client, userdata, msg):
try:
topic_parts = msg.topic.split('/')
if len(topic_parts) < 3:
logger.warning(f"Topic invalide: {msg.topic}")
return
vin = topic_parts[1]
command = msg.payload.decode().strip().lower()
logger.info(f"Commande MQTT reçue pour {vin} : {command}")
# On cherche la config du bon véhicule
vehicle_config = next((v for v in config["vehicle"] if v["vin"] == vin), None)
if vehicle_config is None:
logger.error(f"Aucun véhicule trouvé pour VIN: {vin}")
return
if command == "update":
logger.info(f"Mise à jour forcée demandée pour {vin}")
data = await fetch_renault_data(vehicle_config)
topic = build_topic(vehicle_config)
publish(mqtt_client, topic, json.loads(data))
elif command == "set_charge_start":
logger.info(f"Commande de démarrage de charge reçue pour {vin}")
await set_charge(vehicle_config, start=True)
elif command == "set_charge_stop":
logger.info(f"Commande d'arrêt de charge reçue pour {vin}")
await set_charge(vehicle_config, start=False)
else:
logger.warning(f"Commande inconnue: {command}")
except Exception as e:
logger.error(f"Erreur dans le callback MQTT: {e}")
async def fetch_renault_data(vehicle_config):
async with aiohttp.ClientSession() as websession:
client = RenaultClient(websession=websession, locale=RENAULT_LOCALE)
await client.session.login(RENAULT_EMAIL, RENAULT_PASSWORD)
account = await client.get_api_account(vehicle_config["kamereon_account_id"])
vehicle = await account.get_api_vehicle(vehicle_config["vin"])
battery_status = await vehicle.get_battery_status()
cockpit_info = await vehicle.get_cockpit()
data = {
"battery-status": {
"timestamp": battery_status.timestamp,
"batteryLevel": battery_status.batteryLevel,
"batteryAutonomy": battery_status.batteryAutonomy,
"plugStatus": battery_status.plugStatus,
"chargingStatus": battery_status.chargingStatus,
"chargingRemainingTime": battery_status.chargingRemainingTime
},
"cockpit": {
"totalMileage": cockpit_info.totalMileage
}
}
return data
async def set_charge(vehicle_config, start=True):
try:
async with aiohttp.ClientSession() as websession:
client = RenaultClient(websession=websession, locale=RENAULT_LOCALE)
await client.session.login(RENAULT_EMAIL, RENAULT_PASSWORD)
account = await client.get_api_account(vehicle_config["kamereon_account_id"])
vehicle = await account.get_api_vehicle(vehicle_config["vin"])
if start:
await vehicle.set_charge_start()
logger.info(f"Charge démarrée pour {vehicle_config['vin']}")
else:
await vehicle.set_charge_stop()
logger.info(f"Charge arrêtée pour {vehicle_config['vin']}")
except Exception as e:
logger.error(f"Erreur lors du {'démarrage' if start else 'stop'} de la charge : {e}")
async def main_loop():
global mqtt_client
mqtt_client = init_mqtt()
logger.info("Démarrage du daemon Ze2MQTT")
while True:
for vehicle_config in config["vehicle"]:
try:
vin = vehicle_config["vin"]
kam_id = vehicle_config["kamereon_account_id"]
name = vehicle_config.get("friendly_name", vin)
topic = build_topic(vehicle_config)
data = await fetch_renault_data(vehicle_config)
publish(mqtt_client, topic, data)
except Exception as e:
logger.error(f"Erreur pour le véhicule {vehicle_config['vin']} : {e}")
await asyncio.sleep(INTERVAL)
# Gestion des signaux pour arrêter proprement le daemon
def signal_handler(sig, frame):
logger.info("Arrêt du daemon Ze2MQTT")
mqtt_client.loop_stop()
mqtt_client.disconnect()
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
if __name__ == "__main__":
try:
asyncio.run(main_loop())
except KeyboardInterrupt:
signal_handler(signal.SIGINT, None)
Le fichier de configuration
Je crée ensuite le fichier de configuration config qui permet d’ajuster les véhicules, les paramètres MQTT et le niveau de log..json
nano config.json
Contenu du fichier :
{
"log_level": "WARNING",
"renault": {
"email": "ton_email@example.com",
"password": "ton_mot_de_passe",
"locale": "fr_FR",
"country": "FR"
},
"mqtt": {
"prefix_topic": "RenaultZE",
"broker": "localhost",
"port": 1883,
"username": "mon_utilisateur",
"password": "mon_mot_de_passe"
},
"interval": {
"basic_info_period": 3600
},
"vehicle": [
{
"kamereon_account_id": "47a88a18-5084-4598-9664-1dc9d4737a02",
"vin": "UU1DBG003MU027470",
"friendly_name": "Spring"
}
]
}
Et côté journald, tu règles une limite avec :
mkdir -p /etc/systemd/journald.conf.d nano /etc/systemd/journald.conf.d/ze2mqtt.conf
Contenu du fichier :
[Journal] SystemMaxUse=100M SystemMaxFileSize=10M MaxRetentionSec=1month
Puis :
sudo systemctl restart systemd-journald
Compilation du container
La structure doit ressembler à ça :
. ├── config.json ├── docker-compose.yml ├── dockerfile ├── requirements.txt └── ze2mqtt.py
Maintenant que tous fichiers sont présent dans mon dossier d’installation je lance la compilation :
docker compose build
Puis exécute :
docker compose up -d
Si tu as besoin de modifier le fichier config.json, il te suffit de relancer le container :
docker compose restart ze2mqtt
Et pour consulter le journal (il existe d’autre méthode) :
journalctl CONTAINER_NAME=ze2mqtt -f
Conclusion
Voici une solution robuste RenaultZE2MQTT, reposant sur un script Python exécuté dans un container Docker, utilisant la bibliothèque renault-api. Elle interroge automatiquement une ou plusieurs voitures Renault/Dacia et transmet les statuts vers un broker MQTT.
Elle inclut également un système d’écoute du topic
{MQTT_PREFIX}/{VIN}/commandavec la commandeupdatepour forcer une actualisation à la demande. Les commandesset_charge_startouset_charge_stopquand à elles vont démarrer ou arrêter un cycle de charge.
Par exemple, via une automatisation sur mon téléphone, je déclenche cette commande lorsque je passe en mode de concentration « Conduite », ou encore lorsque je me connecte/déconnecte en Bluetooth au système audio du véhicule.
👉 Cela me permet d’obtenir des statistiques plus précises sur mes trajets et ma consommation.

Merci pour ce super Tuto!!