Daikin2MQTT : piloter ton climatiseur en MQTT avec Python

Préambule

Suite à l’abandon du plugin DaikinOnlineCtrl sous Jeedom, j’ai décidé de reprendre la main.
L’objectif : dialoguer directement avec l’API locale de mes unités Daikin, sans dépendance.

Ce projet repose sur deux scripts distincts :

  • daikin2mqtt_status publie régulièrement l’état des unités (température, mode, conso, etc.)
  • daikin2mqtt_cmd écoute les topics MQTT pour envoyer des commandes aux climatiseurs

Le tout fonctionne sous Docker, avec configuration centralisée via config.json, et publication dans MQTT pour intégration dans Jeedom.

Intégration dans Jeedom

Une fois les scripts en place, je peux créer des objets MQTT dans jMQTT pour chacune de mes unités intérieures.
En fin d’article je partage mon template et mes Widgets utilisés pour composer mes tuiles.

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

(A venir l’article : Le Dockerfile du Destin : une images pour les gouverner tous, pour que tu puisses regrouper tous les bidule2mqtt avec la même image).

Création du containers Docker

Je crée le répertoire d’installation :

mkdir -p ~/docker/daikin2mqtt && cd ~/docker/daikin2mqtt

Création d’un fichier docker-compose.yml :

nano docker-compose.yml

Contenu du fichier :

services:
  daikin2mqtt_status:
    container_name: daikin2mqtt_status
    restart: unless-stopped
    build: ./
    volumes:
      - ./daikin2mqtt_status.py:/app/daikin2mqtt_status.py
      - ./config.json:/app/config.json
    logging:
      driver: journald
    environment:
      - TZ=Europe/Paris
    command: ["python", "daikin2mqtt_status.py"]

  daikin2mqtt_cmd:
    container_name: daikin2mqtt_cmd
    restart: unless-stopped
    build: ./
    volumes:
      - ./daikin2mqtt_cmd.py:/app/daikin2mqtt_cmd.py
      - ./config.json:/app/config.json
    logging:
      driver: journald
    environment:
      - TZ=Europe/Paris
    command: ["python", "daikin2mqtt_cmd.py"]

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 ./

RUN pip install --no-cache-dir -r requirements.txt

Création d’un fichier requirements.txt :

nano requirements.txt

Contenu du fichier :

paho-mqtt
requests

Le script daikin2mqtt_status.py

Ce script interroge régulièrement chaque unité pour :

  • Publier les températures (/aircon/get_sensor_info)
  • Publier les paramètres (/aircon/get_control_info)
  • Publier la consommation hebdo (/aircon/get_week_power)
  • Publier les infos de base (/common/basic_info) une fois par heure

Il se charge aussi d’enrichir automatiquement le config.json avec le SSID de l’unité s’il manque.

nano daikin2mqtt_status.py
import json
import logging
import paho.mqtt.client as mqtt
import requests
import signal
import sys
import time
# Chargement du fichier de configuration JSON
def load_config():
    with open('./config.json', 'r') as f:
        config = json.load(f)
    return config
def save_config(config):
    with open('./config.json', 'w') as f:
        json.dump(config, f, indent=2)
config = load_config()
# Configuration
LOG_LEVEL = config.get("log_level", "INFO").upper()
# Conf -> DAIKIN
INVERTERS = config["inverters"]
STATUS_PERIOD = config["daikin"]["status_period"]
BASIC_INFO_PERIOD = config["daikin"].get("basic_info_period", 3600)
## 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"]
# Configuration du logger
logging.basicConfig(level=getattr(logging, LOG_LEVEL, logging.INFO),
                    format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger("Daikin2MQTT Status")
# 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.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")
    else:
        logger.error(f"Erreur de connexion MQTT : {reason_code}")
def parse_daikin_response(response_text):
    """Convertit la réponse Daikin en dictionnaire Python"""
    try:
        data = {}
        for item in response_text.split(","):
            key, value = item.split("=")
            data[key] = value if value != "-" else None  # Remplace "-" par None
        return data
    except ValueError:
        logging.warning(f"Erreur de parsing de la réponse: {response_text}")
        return None
def fetch_daikin_data(ip, endpoint):
    """Récupère les données depuis une URL spécifique"""
    url = f"http://{ip}{endpoint}"
    try:
        response = requests.get(url, timeout=5)
        if response.status_code == 200:
            return parse_daikin_response(response.text)
        else:
            logging.warning(f"Erreur HTTP {response.status_code} depuis {url}")
            return None
    except requests.RequestException as e:
        logging.warning(f"Erreur de connexion à {url}: {e}")
        return None
def main():
    global mqtt_client
    mqtt_client = init_mqtt()
    logger.info("Démarrage du daemon Daikin2MQTT Status")
    last_basic_info_times = {}  # Stocke les derniers temps de requête pour chaque unité
    modified = False
    # Enrichir les SSID manquants
    for inverter in INVERTERS:
        if not inverter.get("ssid"):
            basic_info = fetch_daikin_data(inverter["ip_addr"], "/common/basic_info")
            if basic_info:
                inverter["ssid"] = basic_info.get("ssid", inverter["friendly_name"])
                logger.info(f"Ajout du SSID '{inverter['ssid']}' pour {inverter['friendly_name']}")
                modified = True
    if modified:
        save_config(config)
        logger.info("config.json mis à jour avec les SSID.")
    while True:
        current_time = time.time()
        for inverter in INVERTERS:
            friendly_name = inverter["friendly_name"]
            ip = inverter["ip_addr"]
            ssid = inverter["ssid"]
            # Vérifie si cette unité doit interroger `basic_info`
            last_query = last_basic_info_times.get(ip, 0)
            if current_time - last_query > BASIC_INFO_PERIOD:
                basic_info = fetch_daikin_data(ip, "/common/basic_info")
                if basic_info:
                    topic = f"{MQTT_PREFIX}/{ssid}/basic"
                    publish(mqtt_client, topic, basic_info, retain=True)
                    topic = f"{MQTT_PREFIX}/{ssid}/friendly_name"
                    publish(mqtt_client, topic, friendly_name, retain=True)
            # Interroge les capteurs en continu
            sensor_data = fetch_daikin_data(ip, "/aircon/get_sensor_info")
            if sensor_data:
                topic = f"{MQTT_PREFIX}/{ssid}/sensor"
                publish(mqtt_client, topic, sensor_data)
            # Interroge le contrôle en continu
            control_data = fetch_daikin_data(ip, "/aircon/get_control_info")
            if control_data:
                topic = f"{MQTT_PREFIX}/{ssid}/control"
                publish(mqtt_client, topic, control_data)
            # Interroge les durée de fonctionnement en continu
            week_power = fetch_daikin_data(ip, "/aircon/get_week_power")
            if week_power:
                topic = f"{MQTT_PREFIX}/{ssid}/week_power"
                publish(mqtt_client, topic, week_power)
                last_basic_info_times[ip] = current_time
        time.sleep(STATUS_PERIOD)
    client.disconnect()
# Gestion des signaux pour arrêter proprement le daemon
def signal_handler(sig, frame):
    logger.info("Arrêt du daemon Daikin2MQTT Status")
    mqtt_client.loop_stop()
    mqtt_client.disconnect()
    sys.exit(0)
if __name__ == "__main__":
    signal.signal(signal.SIGINT, signal_handler)
    signal.signal(signal.SIGTERM, signal_handler)
    main()

Le script daikin2mqtt_cmd.py

Ce second script se contente d’un rôle : réagir aux commandes MQTT reçues.

nano daikin2mqtt_cmd.py
import asyncio
import json
import logging
import paho.mqtt.client as mqtt
import requests
import signal
import sys
import time
# 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 -> DAIKIN
INVERTERS = config["inverters"]
## 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"]
# Configuration du logger
logging.basicConfig(level=getattr(logging, LOG_LEVEL, logging.INFO),
                    format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger("Daikin2MQTT Command")
# 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 = on_message
    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")
        topic = f"{MQTT_PREFIX}/+/command"  # Topic pour recevoir les commandes
        client.subscribe(topic)
        logger.info(f"Souscription au topic de commande : {topic}")
    else:
        logger.error(f"Erreur de connexion MQTT : {reason_code}")
# Fonction pour lire les status de l'unité Daikin
def get_control_info(ip):
    try:
        url = f"http://{ip}/aircon/get_control_info"
        response = requests.get(url, timeout=5)
        if response.status_code == 200:
            data = dict(x.split('=') for x in response.text.strip().split(','))
            return data
        else:
            logger.error(f"Erreur HTTP {response.status_code} lors du get_control_info")
    except requests.RequestException as e:
        logger.error(f"Erreur de connexion à {url}: {e}")
    return {}
# Fonction pour envoyer les commandes à l'unité Daikin
def send_control_command(ip, command):
    url = f"http://{ip}/aircon/set_control_info"
    try:
        response = requests.get(url, params=command, timeout=5)
        if response.status_code == 200:
            logger.debug(f"Commande envoyée avec succès: {command}")
        else:
            logger.error(f"Erreur HTTP {response.status_code} lors de l'envoi de la commande")
    except requests.RequestException as e:
        logger.error(f"Erreur de connexion à {url}: {e}")
# Fonction pour traiter les messages MQTT
def on_message(client, userdata, msg):
    try:
        # Décoder le JSON reçu
        command = json.loads(msg.payload.decode())
        ssid = msg.topic.split("/")[1]
        inverter = next((inv for inv in INVERTERS if inv["ssid"] == ssid), None)
        if not inverter:
            logger.error(f"L'unité avec SSID '{ssid}' non trouvée dans la configuration")
            return
        ip = inverter["ip_addr"]
        current_state = get_control_info(ip)
        if not current_state:
            logger.error(f"Impossible de récupérer l'état actuel de l'unité {ssid}")
            return
        # Priorité au mode fourni dans la commande, sinon celui du state
        mode = str(command.get("mode", current_state.get("mode", "3")))
        # Sécurisation température
        if current_state.get("stemp") in ("--", ""):
            fallback_stemp = current_state.get(f"dt{mode}")
            if fallback_stemp and fallback_stemp not in ("--", ""):
                current_state["stemp"] = fallback_stemp
                logger.debug(f"Valeur 'stemp' remplacée par fallback dt{mode} = {fallback_stemp}")
            else:
                logger.warning(f"Température de fallback indisponible pour mode {mode}, valeur 'stemp' non corrigée")
        # Sécurisation humidité
        if current_state.get("shum") in ("--", ""):
            current_state["shum"] = "0"
            logger.debug(f"Valeur 'shum' remplacée par défaut = 0")
        # Fusion des états
        merged_command = {
            "pow": current_state.get("pow", "1"),
            "mode": current_state.get("mode", "3"),
            "stemp": current_state.get("stemp", "22"),
            "shum": current_state.get("shum", "0"),
            "f_rate": current_state.get("f_rate", "A"),
            "f_dir": current_state.get("f_dir", "0")
        }
        # Mise à jour des champs fournis dans la commande MQTT
        for key, value in command.items():
            merged_command[key] = str(value)
        logger.debug(f"Commande complète envoyée à {ssid}: {merged_command}")
        send_control_command(ip, merged_command)
        # Petite pause pour laisser l'unité appliquer la commande
        time.sleep(1)
        # Lecture du nouvel état réel de l'unité
        control_data = get_control_info(ip)
        if control_data:
            publish(client, f"{MQTT_PREFIX}/{ssid}/control", control_data)
        else:
            logger.warning(f"Échec de récupération du nouvel état de l’unité {ssid}")
    except Exception as e:
        logger.error(f"Erreur de traitement du message MQTT: {e}")
# Signal handler
running = True
def signal_handler(sig, frame):
    global running
    logger.info("Arrêt du daemon Daikin2MQTT Command")
    running = False
# Boucle principale async
async def main_loop():
    global mqtt_client
    mqtt_client = init_mqtt()
    logger.info("Démarrage du daemon Daikin2MQTT Command")
    while running:
        await asyncio.sleep(1)
    mqtt_client.loop_stop()
    mqtt_client.disconnect()
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
if __name__ == "__main__":
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    try:
        loop.run_until_complete(main_loop())
    finally:
        loop.close()

Le fichier de configuration

Je créé ensuite le fichier de configuration config.json qui permet d’ajuster les unités, les paramètres MQTT et le niveau de log.

nano config.json

Contenu du fichier :

{
  "log_level": "WARNING",
  "mqtt": {
    "prefix_topic": "daikin",
    "broker": "192.168.52.6",
    "port": 1883,
    "username": "",
    "password": ""
  },
  "daikin": {
    "status_period": 900,
    "basic_info_period": 3600
  },
  "inverters": [
    {
      "ip_addr": "192.168.52.22",
      "friendly_name": "chambre_verte",
      "ssid": ""
    },
    {
      "ip_addr": "192.168.52.23",
      "friendly_name": "chambre_jaune",
      "ssid": ""
    },
    {
      "ip_addr": "192.168.52.24",
      "friendly_name": "bureau",
      "ssid": ""
    },
    {
      "ip_addr": "192.168.52.25",
      "friendly_name": "chambre_bleue",
      "ssid": ""
    },
    {
      "ip_addr": "192.168.52.26",
      "friendly_name": "chambre_orange",
      "ssid": ""
    }
  ]
}

Et côté journald, tu règles une limite avec :

mkdir -p /etc/systemd/journald.conf.d
nano /etc/systemd/journald.conf.d/daikin2mqtt_cmd.conf
nano /etc/systemd/journald.conf.d/daikin2mqtt_status.conf

Contenu des fichier :

[Journal]
SystemMaxUse=100M
SystemMaxFileSize=10M
MaxRetentionSec=1month

Puis :

sudo systemctl restart systemd-journald

Compilation du container

La structure doit ressembler à ça :

.
├── config.json
├── daikin2mqtt_cmd.py
├── daikin2mqtt_status.yml
├── docker-compose.yml
├── dockerfile
└── requirements.txt

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 daikin2mqtt_cmd
docker compose restart daikin2mqtt_status

Et pour consulter le journal (il existe d’autre méthode) :

journalctl CONTAINER_NAME=daikin2mqtt_cmd -f
journalctl CONTAINER_NAME=daikin2mqtt_status -f

Structure des messages de commandes JSON :

Le message MQTT doit est envoyé dans le topic spécifique comme /daikin/DaikinAP123456/command, avec un JSON.

Chaque message JSON pourra contenir une seule commande comme suit :

Pour allumer ou éteindre l’unité (ON/OFF) : { "pow": 1 // ou 0 pour éteindre }

Pour changer le mode (froid, chaud, auto, etc.) : { "mode": 3 // 0=Auto, 2=Séchage, 3=Refroidissement, 4=Chauffage, 6=Ventilation }

Pour ajuster la température cible : { "stemp": 22 // Température cible en °C }

Pour ajuster la vitesse du ventilateur : { "f_rate": 4 // A=Auto, B=Silence, 3=Vit.1, 4=Vit.2, 5=Vit.3, 6=Vit.4, 7=Vit.5 }

Pour ajuster la direction des volets : { "f_dir": 2 // 0=Arrêt, 1=Verticale, 2=Horizontale, 3=3D }

Toutefois l’API locale require un message contenant les 5 valeurs. Si tu n’envoie qu’une commande dans ton JSON le script va s’occupe de completer le message pour que l’appel HTTP sur l’unité Daikin puisse aboutir.

Conclusion

Avec ce duo de scripts, je peux supprimer l’extension DaikinOnlineCtrl de Jeedom. Je garde un contrôle complet, sans cloud. Chaque unité est entièrement pilotable depuis MQTT, avec une remontée d’état fiable et des commandes instantanées.

Voici le modèle d’équipement jMQTT que j’ai composé :

Et les Widgets utilisé :

Sources :
https://github.com/ael-code/daikin-control
https://github.com/SMerrony/daikin2mqtt

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *