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_statuspublie 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 qui permet d’ajuster les unités, les paramètres MQTT et le niveau de log..json
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
