Sommaire
Cet article s'adresse à des utilisateurs expérimentés de Home Assistant et de Proxmox.
Compte tenu du nombre important de briques techniques à déployer (virtualisation, conteneurs, protocoles MCP, orchestrateur Dify, requêtes API et scripts), il ne s'agit pas d'un tutoriel pas-à-pas pour débutants. Certains concepts d'architecture réseau ou d'administration système seront survolés et supposés maîtrisés afin de concentrer le propos sur la logique d'intégration Dify/HA. Pour la mise en œuvre détaillée, faites-vous aidez par votre IA favorite.
Chapitre 1 : Introduction et cas d'usage
Les limites des automatisations classiques sous Home Assistant
Toute personne ayant poussé la domotique au-delà de la simple ampoule connectée s'est heurtée à ce plafond de verre : gérer du raisonnement contextuel dans Home Assistant devient vite un cauchemar de maintenance.
Pour illustrer ce constat, prenons un exemple très classique : l'arrosage automatique du jardin.
Une automatisation basique déclenche un relais pendant 15 minutes tous les soirs à 20h00. Rapidement, on cherche à l'affiner :
- Ne pas arroser s'il a plu aujourd'hui.
- Ajuster la durée si la température a dépassé 30 °C.
- Annuler l'arrosage si des pluies significatives sont annoncées pour demain.
Sous Home Assistant, la mise en œuvre traditionnelle implique d'empiler :
- Des templates Jinja2 illisibles pour extraire des historiques ou calculer des deltas de précipitations.
- Des usines à gaz de variables, de
choose, de conditions imbriquées et de scripts d'aide. - Des règles rigides qui ne prennent pas en compte les nuances (ex. 2 mm de pluie fine étalés sur 12 h n'ont pas le même impact que 10 mm tombés en un orage de 15 minutes).
Dès que l'on veut injecter une vraie dose d'analyse — analyser une tendance météo globale sur 3 jours, ajuster une durée de manière continue plutôt que par paliers fixes, ou rédiger un compte rendu clair —, le code Jinja/YAML atteint ses limites logiques.
Le cas d'usage : Un arrosage contextualisé et piloté par IA
L'objectif de cet article est de déporter cette charge de réflexion vers un orchestrateur d'IA agentique : Dify. Dify est une alternative en source ouverte de N8N.
Au lieu de coder des règles statiques, nous allons confier à un workflow Dify le soin d'analyser la situation et de prendre la meilleure décision.

Le fonctionnement global
- Raisonnement & Calcul : Un modèle de langage (LLM) collecte les entités météo de Home Assistant via une interface de type MCP (Model Context Protocol) et analyse ces métriques en appliquant des directives métier souples (« Si la terre est déjà trempée ou si un orage arrive demain, annule. Si la journée a été caniculaire sans pluie, augmente le temps de 30 % »). Il détermine dynamiquement la durée optimale d'arrosage (en minutes).
- Pilotage de la pompe : Dify envoie la commande d'activation du relais de la pompe à Home Assistant pour la durée exacte calculée (ou dans notre cas, déclenche une automatisation qui effectue ce travail).
- Aiguillage & Actions dans Home Assistant :
- Cas 1 (Durée < 1 minute) : Le workflow conclut que le sol est suffisamment humide. Aucun déclenchement de pompe n'est effectué, et Dify envoie un email à l'utilisateur pour lui expliquer pourquoi l'arrosage a été ignoré.
- Cas 2 (Durée ≥ 1 minute) : Dify pilote le relais de la pompe pour la durée exacte calculée, puis envoie un email récapitulatif précisant le temps d'activation et la justification météo…
L'intérêt majeur de cette approche est d'obtenir un pipeline extrêmement flexible, qui tient en moins de 10 nœuds, sans aucune ligne de code Jinja complexe dans Home Assistant, tout en restant facile à faire évoluer. Le LLM s'adapte tout seul aux différentes conditions sans que vous ayez explicitement à développer ses règles. C'est la grande force de l'IA.
Chapitre 2 : Présentation des composants et de leur rôle
Pour réaliser ce cas d'usage sans écrire des lignes de code Jinja à rallonge, l'architecture repose sur quatre composants clés qui collaborent de manière transparente :

Home Assistant : Le cœur domotique et la source de vérité
Home Assistant conserve son rôle naturel : gérer le matériel local et remonter l'état des capteurs.
- Capteurs météo : Il fournit l'historique de température, le cumul de pluie passé (24-48h) et les prévisions météo à venir.
- Actuateur : Il gère le relais physique (ou le script) connecté à la pompe d'arrosage.
L'application MCP Home Assistant : La passerelle standardisée
Le protocole MCP (Model Context Protocol) agit comme un traducteur universel entre le LLM et Home Assistant.
- Plutôt que de développer des appels API REST complexes ou des Web Sockets personnalisés, l'app MCP expose proprement les entités, états et services de Home Assistant sous forme d'outils (Tools) directement exploitables par l'IA.
- Grâce au MCP, l'agent IA peut lire l'état de n'importe quel capteur météo ou exécuter un service (ex.
script.turn_onouswitch.turn_on) de façon standardisée.
Le Modèle de Langage (LLM) : Le moteur de raisonnement
Le LLM est la brique d'intelligence. Il reçoit en entrée les données brutes lues via le MCP et exécute la logique métier selon les instructions du prompt système :
- Il évalue la cohérence des données météo.
- Il pondère le besoin en eau (ex. annuler si une grosse pluie est annoncée, augmenter si les températures ont été très élevées).
- Il retourne un résultat structuré (durée d'arrosage en minutes, statut
arrosage_necessaire[Vrai/Faux] et une explication claire en français).
Dify : L'orchestrateur de workflow agentique
Dify est le chef d'orchestre visuel (no-code / low-code) qui assemble l'ensemble :
- Déclenchement : Il lance le workflow selon un calendrier défini (ex. tous les soirs à 22 h 00).
- Exécution de l'Agent : Il passe les outils MCP à l'agent IA pour récupérer les états météo de Home Assistant.
- Aiguillage Conditionnel : Un nœud Si/Sinon (IF/ELSE) oriente le flux selon le résultat du calcul (
durée < 1 minoudurée ≥ 1 min). - Actions de sortie : Dify gère l'envoi des notifications par email et envoie l'ordre final d'activation de la pompe à Home Assistant via l'API REST/MCP.
Gmail : un nœud Dify permettant l'envoi de mail via Gmail
Dify possède un nombre impressionnant de nœuds grâce à une très grande communauté. Un nœud Gmail permet l'envoi d'email. Pour ce tuto, nous utiliserons finalement le nœud Email simple qui permet la conversion Markdown HTML ce qui va nous permettre de mettre en forme notre mail.
Chapitre 3 : L'installation et la configuration de l'application HA MCP (ha_mcp)
Pour que Dify (ou tout autre orchestrateur basé sur un LLM) puisse interagir nativement avec votre instance Home Assistant, il faut exposer l'API de Home Assistant sous forme de serveur MCP (Model Context Protocol).
Bonne nouvelle : pas besoin de sortir la grosse artillerie, l'intégration se fait très facilement via l'application Home Assistant ha_mcp.
Qu'est-ce que l'application ha_mcp ?
Cette application fait le pont entre le protocole MCP et l'API de Home Assistant. Il met automatiquement à disposition du LLM une série d'outils (Tools) prédéfinis :
get_state/get_states: Permet à l'agent de lire la valeur instantanée ou l'état de n'importe quel capteur (weather.maison,sensor.pluie_24h, etc.).call_service: Permet à l'agent d'exécuter une action dans Home Assistant (ex. appelerscript.turn_on,switch.turn_onou envoyer une notification).list_services/get_history: Permet à l'agent d'explorer les services disponibles ou d'analyser l'historique d'une entité.
Installation de l'application ha_mcp
- Dans Home Assistant, rendez-vous dans Paramètres > Application.
- Cliquez sur Installer une application en bas à droite.
- Si le dépôt n'est pas présent par défaut, ajoutez l'URL du dépôt GitHub contenant
ha_mcpvia les trois petits points en haut à droite (Dépôts / Repositories). - Cherchez ha_mcp, cliquez dessus puis faites Installer.
Configuration et démarrage
Une fois le module installé :
- Accédez à l'onglet Configuration de l'application.
- Vérifiez le port d'écoute exposé (par exemple le port 8000 ou celui configuré pour le transport SSE - Server-Sent Events).
- Cochez Lancer au démarrage et Restaurer pour garantir la haute disponibilité.
- Cliquez sur Démarrer.
Note : Étant exécuté en tant qu'application natif, ha_mcp s'interface directement avec l'API interne de Home Assistant sans que vous n'ayez à gérer manuellement de jetons d'accès ou de règles de pare-feu complexes entre conteneurs distincts.Vérification
Rendez-vous dans l'onglet Journal (Logs) de l'application : vous devez voir le serveur MCP démarré et à l'écoute des requêtes SSE. L'URL d'accès au serveur MCP pour Dify sera simplement : http://<IP_DE_VOTRE_HA>:8000/sse
Chapitre 4 : L'installation de Dify sur Proxmox (Retour d'expérience)
Installer Dify sur un environnement de virtualisation comme Proxmox VE permet de bénéficier d'une infrastructure robuste et isolée. Cependant, la mise en place peut réserver quelques surprises et s'avérer un peu laborieuse si l'on ne respecte pas certains prérequis.
Voici les étapes pas-à-pas et les pièges à éviter pour un déploiement fluide.
Choix du type de déploiement : LXC vs VM
Deux options s'offrent à vous sous Proxmox :
- Un conteneur LXC (Debian/Ubuntu) avec Docker : Solution légère en ressources (RAM/CPU), idéale si votre nœud Proxmox est dimensionné de façon raisonnable.
- Une machine virtuelle (VM) sous Debian/Ubuntu : Légèrement plus gourmande, mais offre une isolation stricte et évite les soucis de compatibilité de modules noyau Docker dans LXC.
nesting=1) dans les options du conteneur Proxmox, ainsi que le privilège Keyctl si nécessaire, sinon Docker refusera de démarrer ses conteneurs enfants.Dans la suite de ce chapitre, on va détailler la version avec LXC.
2. Déploiement via Docker Compose
Dify se compose de multiples micro-services (API, Web UI, PostgreSQL, Redis, Weaviate/Vector database, Celery workers, etc.). L'installation officielle via docker-compose est la méthode la plus fiable.
Cloner le dépôt officiel Dify :
git clone https://github.com/langgenius/dify.git
cd dify/docker
Configurer l'environnement : Copiez le fichier d'exemple pour créer votre fichier d'environnement :
cp .env.example .env
Il va falloir paramétrer le fichier .env avant de démarrer. Pour exemple voici les paramètres que j'ai du modifier :
# Core service URLs
CONSOLE_API_URL=https://dify.domaine.fr
SERVER_CONSOLE_API_URL=http://api:5001
CONSOLE_WEB_URL=https://dify.domaine.fr
SERVICE_API_URL=https://dify.domaine.fr
TRIGGER_URL=http://localhost
APP_API_URL=https://dify.domaine.fr
APP_WEB_URL=https://dify.domaine.fr
FILES_URL=
INTERNAL_FILES_URL=
ENDPOINT_URL_TEMPLATE=https://dify.domaine.fr/e/{hook_id}
NEXT_PUBLIC_SOCKET_URL=wss://dify.domaine.fr
# Comment this ALLOW_INSECURE_CONNECTIONS=true
# To allow MCP to Home Assistant
SSRF_DEFAULT_ALLOW_IPS=<IP Ha>
CODE_EXECUTION_ENGINE=docker
SANDBOX_HOST=sandbox
SANDBOX_PORT=8194
DIFY_SHELL_PROVIDER=docker
DIFY_AGENT_SHELLCTL_ENTRYPOINT=http://local_sandbox:5004
TIMEZONE=Europe/ParisDans ce .env, dify.domaine.fr est l'adresse d'accès publique de votre instance Dify. C'est requis si vous voulez utiliser Dify pour les URL callback OAuth2. Par exemple, Gmail a besoin d'accéder à votre URL de callback dans Dify. Vous pouvez garder une URL locale si vous n'avez pas besoin de callback OAuth2.
N'exposez jamais directement votre instance Dify sur Internet. Un reverse proxy type Nginx ou tout autre moyen d'exposer une URL interne de façon sécurisée est nécessaire.
Lancer la stack :
docker compose up -d
En cas de soucis, vérifiez les logs du Worker :
docker compose logs -f worker --tail 100Encore une fois faites vous aider par votre IA favorite pour corriger les différents warning. Cette phase a été assez longue pour avoir une configuration opérationnelle.
Les pièges à éviter et retours d'expérience
Consommation de mémoire RAM
Dify est une plateforme complète et gourmande au démarrage. Prévoyez au minimum 4 Go de RAM (6 à 8 Go recommandés) dédiés au conteneur ou à la VM Proxmox. En deçà, les conteneurs PostgreSQL ou la base vectorielle risquent de subir des OOM Kills (Out Of Memory) aléatoires.
Problèmes de résolution DNS et réseau local (HA / MCP)
Si Dify tourne dans un LXC/VM et Home Assistant sur une autre machine/VM du réseau :
- Assurez-vous que Dify peut joindre l'adresse IP locale de Home Assistant et de l'application MCP (
[http://192.168.1.](http://192.168.1.)XXX:8000). - Attention aux règles de pare-feu Proxmox (Proxmox Firewall) qui peuvent bloquer le flux HTTP entrant/sortant entre deux conteneurs du même bridge.
Gestion des variables d'URL (CONSOLE_WEB_URL & APP_WEB_URL)
Si vous utilisez un reverse proxy (comme Nginx Proxy Manager ou Traefik) devant Dify pour gérer le HTTPS, veillez à bien renseigner les variables d'URL dans le fichier .env :
Extrait de code (cf. variables d'env au-dessus)
CONSOLE_WEB_URL=https://dify.domaine.fr
APP_WEB_URL=https://dify.domaine.fr
Une mauvaise configuration de ces variables peut entraîner des erreurs CORS ou des redirections en boucle lors de l'accès à la console d'administration.
Validation de l'installation
Une fois tous les conteneurs démarrés (docker compose ps), ouvrez votre navigateur sur http://<IP_PROXMOX_DIFY>:3000 (ou l'URL configurée). La mire de création du compte administrateur initial de Dify doit s'afficher.
Avant d'aller plus loin, il est conseillé de suivre quelques tutos pour prendre en main Dify si vous ne l'avez jamais fait. Youtube est votre ami.
Chapitre 5 : La configuration d'un agent Dify et des compétences (Skills)
Maintenant que l'infrastructure est en place, nous allons déclarer l'application ha_mcp dans Dify comme une compétence (Tool) puis configurer l'Agent IA chargé de la prise de décision.
Connexion du serveur MCP à Dify
Dans Dify, les compétences s'appuient sur le protocole MCP pour mettre à disposition des outils d'action au modèle.
- Allez dans le panneau d'administration Dify : Intégration > MCP > Ajouter un MCP (http)

- Indiquez l'URL d'écoute du serveur MCP Home Assistant :
http://<IP_HOME_ASSISTANT>:8000/sse - Donnez lui un nom
- Validez : Dify découvre automatiquement la liste des outils exposés par
ha_mcp(get_state,call_service, etc.).
Création de l'Agent « Arrosage Agent »
Dans Dify, créez un nouvel Agent depuis le menu agent, et nommez-le Arrosage agent.
Choix du Modèle
Sélectionnez un modèle rapide et performant en raisonnement structuré, par exemple OpenAI GPT-luna qui est performant rapide et pas cher. Beaucoup de modèles sont disponibles, y compris des modèles locaux tournant sous Ollama.
Donnez-lui le prompt système
Par exemple :
Tu es un assistant qui doit calculer la durée d'arrosage.
Les règles de calculs et les entités nécessaires sont expliquées dans tes compétences. Tu dois suivre ces règles de façon impérative.
Tu dois utiliser Home Assistant pour aller chercher les entités nécessaires au calcul.
Tu calculeras la durée d'arrosage en utilisant des règles de bon sens qui dépendent des conditions météo passée, présentes et futures.
Respecte bien le json schema de sortie. Indique toujours les champs:
1. `arrosage_necessaire`,
2. `duree_arrosage_minutes`,
3. `explication`
4. `erreur`Les compétences
Pour guider le modèle, il est important de lui donner des compétences (SKILL). Un SKILL est un fichier Markdown qui donne des consignes au LLM.
Voici par exemple mon fichier SKILL nommé arrosage.md qui explique tout ce que le LLM doit savoir sur mon infrastructure d'arrosage :
---
name: arrosage
description: "Gestion de l'arrosage automatique de la maison : entités Home Assistant (pompe d'arrosage, pompe de remplissage de la citerne, météo), règles absolues de sécurité (pluie, température, durées min/max). À utiliser pour créer, modifier ou diagnostiquer une automatisation d'arrosage."
---
# Gestion de l'arrosage
Ce skill décrit le système d'arrosage automatique de la maison : les entités Home Assistant à utiliser et les règles absolues qui doivent toujours être respectées, quelle que soit l'automatisation ou le script concerné.
## 1. Les entités Home Assistant
Les entités utilisables pour la gestion de l'arrosage sont :
| Entité | Rôle | Utilisation |
| -------------------------------------------------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `switch.prise_pompe_arrosage` | Pompe d'arrosage | Prise pilotant la pompe d'arrosage du jardin. Doit être `on` pendant l'arrosage, `off` en dehors. |
| `switch.prise_pompe_reservoir` | Pompe de remplissage de la citerne | Prise pilotant la pompe de remplissage de la citerne. **Profite de l'arrosage pour remplir la citerne** : à activer en même temps que la pompe d'arrosage. |
| `weather.beynes` | Météo locale (Météo-France) | Météo courante et prévisions. `attributes.temperature` = température courante. |
| `sensor.meteo_beynes_precipitation` | Précipitations courantes | Précipitations du moment (mm), en continu. Utilisé pour la pluie du jour. |
| `sensor.beynes_daily_precipitation` | Précipitations cumulées du jour | Cumul de précipitations de la journée (mm), source Météo-France. Source principale de la règle 1. |
| `sensor.archive_meteo_beynes_precipitation` | Archive pluviométrie journalière | Pluviométrie journalière historique : les valeurs par date (365 jours) sont dans les **attributs**, la clé de chaque attribut est la date (ex. `2026-09-12T00:00:00Z`). Source des règles 1 et 5. |
| `sensor.archive_meteo_beynes_temperature` | Archive température max journalière | Température max journalière historique : valeurs par date dans les **attributs** (même mécanisme). Source de la règle 2 (moyenne mobile). |
| `weather.beynes` (service `weather.get_forecasts`) | Prévisions météo J+1 | Appel de service `weather.get_forecasts` sur `weather.beynes` avec `type: daily` et `return_response: true`. Le 1er élément de `forecast` = aujourd'hui, le **2e élément = demain (J+1)**, avec `temperature` (max), `templow`, `precipitation` (mm), `condition`. Source des règles 2 et 5 (terme futur de l'EMA) et du critère d'ensoleillement (`condition` J+1). |
| `weather.beynes` (historique via recorder) | Historique des conditions météo | Historique des états de `weather.beynes` sur les 3 jours précédents (changement d'états horodatés). Source du calcul du taux d'ensoleillement journalier (`ha_get_history` / API history). |
| `sun.sun` | Lever/coucher du soleil | Attributs `next_rising` / `next_setting` (et historique) pour calculer la **durée du jour**. Utilisée pour convertir l'EMA des taux d'ensoleillement en heures d'ensoleillement (critère de durée). |
### Notes d'implémentation
- Toujours vérifier que l'entité n'est pas dans un état `unknown` ou `unavailable` avant de lancer un cycle. En cas d'indisponibilité, ne pas arroser (échec dans l'état sûr).
- Utiliser `target.entity_id` avec des identifiants littéraux ; ne jamais utiliser `device_id` ni `{{ this.entity_id }}` dans les automatisations.
- Les durées et seuils réglables doivent être portés par des helpers (`input_number`, `input_boolean`, `timer`) plutôt que codés en dur dans plusieurs automatisations.
## 2. Les règles absolues
Ces règles sont **non négociables**. Toute automatisation, tout script ou toute suggestion doit les respecter. Si une règle n'est pas vérifiable (capteur indisponible, donnée absente), l'arrosage est annulé.
### Règle 1 — Pas d'arrosage après la pluie
> **Ne jamais arroser s'il a plu plus de 2 mm dans la journée.**
- Source : cumul de précipitations du jour (`sensor.beynes_daily_precipitation`, complété par `sensor.meteo_beynes_precipitation` pour le temps réel).
- Traduction recommandée : condition `numeric_state` sur `sensor.beynes_daily_precipitation` avec `above: 2` (condition d'inhibition), plutôt qu'un modèle Jinja.
### Règle 2 — Pas d'arrosage par temps froid
> **Ne jamais arroser si la moyenne mobile exponentielle (EMA) des températures max est inférieure à 20 °C.**
- Il s'agit d'une **EMA anticipative** (cf. section « Définition de l'EMA ») des températures max, calculée à partir de `sensor.archive_meteo_beynes_temperature` (passé) et de la prévision J+1 de `weather.get_forecasts` (terme futur).
- Si l'EMA des températures max est `< 20 °C`, l'arrosage est interdit (sol froid, croissance ralentie, risque de gel nocturne).
- Les jours anciens comptent moins : une journée froide isolée il y a 3 jours n'inhibe pas l'arrosage si les jours récents sont chauds, et inversement une fraîcheur récente pèse plus.
- Traduction recommandée : un helper capteur de modèle unique calculant l'EMA (à privilégier après avoir écarté les helpers natifs `statistics`/`average`), puis condition `numeric_state` `above: 20` sur ce helper.
- Tester tout modèle avec `ha_eval_template` avant de l'intégrer.
### Règle 3 — Durée maximale
> **Ne jamais arroser plus de 15 minutes.**
- Durée maximale absolue d'un cycle, même si une durée calculée est plus courte, même en cas de relance.
- Doit être garantie par un mécanisme indépendant du déroulement nominal : `timer` par zone avec arrêt à expiration, ou temporisation équivalente. Ne jamais compter uniquement sur la logique nominale.
- L'inhibition générale (`input_boolean`) doit permettre de couper immédiatement toute arrosage en cours.
### Règle 4 — Durée minimale
> **Ne jamais arroser moins de 2 minutes.**
- Un cycle inférieur à 2 minutes est interdit : arrosage inutile (sol non mouillé en profondeur) et sollicitation matérielle sans bénéfice.
- Si le besoin calculé est inférieur à 2 minutes, **ne pas arroser du tout** plutôt que d'arroser trop peu. Ne jamais « arrondir » à 2 minutes si le besoin réel est moindre.
### Règle 5 — Sol encore humide après des pluies récentes
> **Ne pas arroser si l'EMA de la pluviométrie journalière dépasse 2 mm.**
- Il s'agit d'une **EMA anticipative** (cf. section « Définition de l'EMA ») de la pluviométrie journalière, calculée à partir de `sensor.archive_meteo_beynes_precipitation` (passé) et de la prévision J+1 de `weather.get_forecasts` (terme futur).
- Règle d'inhibition complémentaire à la règle 1 : la règle 1 porte sur le jour seul, cette règle 5 tient compte des pluies récentes avec un poids décroissant selon l'ancienneté (une pluie d'hier pèse encore la moitié d'une pluie d'aujourd'hui, une pluie d'avant-hier un quart).
- Traduction recommandée : helper capteur de modèle calculant l'EMA, puis condition `numeric_state` `above: 2` sur ce helper.
### Définition de l'EMA (moyenne mobile exponentielle anticipative)
Les règles 2 et 5 utilisent une **moyenne mobile exponentielle** qui pondère chaque jour selon son ancienneté avec des poids géométriques de raison ½, **et intègre la prévision du lendemain (J+1) avec un poids réduit** : le futur n'est pas certain, il ne doit avoir qu'une influence palpable mais faible, égale à celle de T(j-2).
$$EMA = \frac{0{,}5 \cdot V(j) + 0{,}25 \cdot V(j-1) + 0{,}125 \cdot V(j-2) + 0{,}125 \cdot V(j+1)}{1}$$
où $V(j)$ est la valeur du jour courant, $V(j-1)$ celle de la veille, $V(j-2)$ celle d'avant-hier et $V(j+1)$ la **prévision** du lendemain.
- **La somme des poids fait exactement 1** : aucune renormalisation n'est nécessaire, l'EMA reste directement comparable au seuil.
- **Signification des poids** : le passé récent pèse 0,5 / 0,25 / 0,125 ; le futur pèse 0,125 — le même poids que T(j-2). Une chaleur ou une pluie annoncée pour demain influence la décision sans jamais la renverser seule.
- **Terme futur** : pour la température, prendre la prévision `temperature` (T max) ; pour la pluviométrie, la prévision `precipitation` (mm). Ces valeurs sont le **2e élément** de la liste `forecast` retournée par `weather.get_forecasts` sur `weather.beynes` avec `type: daily` (le 1er élément = aujourd'hui).
- Si l'appel de prévision échoue ou est indisponible, **omettre le terme futur et renormaliser** (diviser par 0,875) plutôt que d'annuler : l'EMA dégradée reste utilisable.
- Ne coder ce calcul qu'une seule fois, dans un helper capteur de modèle dédié par règle ; ne jamais dupliquer le modèle Jinja dans plusieurs automatisations.
- `weather.get_forecasts` étant un appel de service (et non un attribut statique), prévoir une automatisation `time_pattern` qui l'appelle régulièrement (avec `response_variable`) et stocke le résultat dans des helpers, pour que les capteurs d'EMA aient toujours accès au terme futur.
### Critère d'ensoleillement (modulation de la durée, pas une règle absolue)
L'ensoleillement n'est **ni une règle d'inhibition ni une règle de durée min/max** : c'est un **critère d'affinage** de la durée d'arrosage une fois que les règles 1 à 5 autorisent l'arrosage.
> **Durée d'arrosage ajustée en fonction de l'EMA des heures d'ensoleillement des derniers jours (et du lendemain).**
#### Calcul du taux d'ensoleillement journalier E(j)
À partir de l'historique de `weather.beynes` (états textuels horodatés sur la journée), mapper chaque condition sur un taux d'ensoleillement :
| Condition (`weather.beynes`) | Taux d'ensoleillement |
| -------------------------------------------------- | --------------------- |
| `sunny` | 1,0 |
| `partlycloudy` | 0,5 |
| `cloudy` | 0,2 |
| `fog` | 0,15 |
| `rainy`, `pouring`, `snowy`, `hail`, `snowy-rainy` | 0,0 |
| `clear-night` (ou tout autre) | 0,0 |
La moyenne journalière est **pondérée par la durée** de chaque état (moyenne des taux × durée de l'état, sur la journée) — pas une simple moyenne d'échantillons.
#### EMA des taux
$$EMA_{taux} = 0{,}5 \cdot E(j) + 0{,}25 \cdot E(j-1) + 0{,}125 \cdot E(j-2) + 0{,}125 \cdot E(j+1)$$
- $E(j+1)$ = taux de la **prévision** du lendemain (champ `condition` du 2e élément de `forecast` de `weather.get_forecasts`).
- Même mode dégradé que les autres EMA : si la prévision est indisponible, omettre le terme futur et diviser par 0,875.
- Pour E(j), le jour en cours : calculer sur la fenêtre jour écoulée (lever du soleil → maintenant).
#### Conversion en heures d'ensoleillement
La durée du jour varie selon la saison (une journée ensoleillée du 21 juin dure plus longtemps qu'une d'octobre), donc l'EMA est exprimée en **heures d'ensoleillement** :
$$EMA_{ensoleillement} = \text{durée du jour} \times EMA_{taux}$$
- La durée du jour provient de `sun.sun` (lever/coucher).
- **On suppose que les 4 jours du calcul (j-2 à j+1) ont la même durée du jour** : utiliser la durée du jour courant pour l'ensemble du calcul.
- Pour le jour J, le calcul intégral est : $E(j)$ en heures $= \text{durée du jour} \times EMA_{taux}(j)$... en pratique on calcule directement l'EMA des taux puis on multiplie par la durée du jour une seule fois, a priori identique pour les 4 jours.
#### Utilisation dans le choix de la durée
- **Sens de l'ajustement** : plus l'EMA d'ensoleillement est élevée, plus le sol s'est assché par évaporation ces derniers jours, et plus la durée d'arrosage doit être **allongée** ; inversement, un ensoleillement faible (gris, pluvieux) réduit le besoin, donc **raccourcit** la durée. Les coefficients exacts relèvent de la stratégie de l'automatisation, qui doit rester conforme aux règles 3 et 4 (2 à 15 minutes).
- Ce critère **ne peut jamais** autoriser un arrosage interdit par les règles 1, 2 ou 5, ni dépasser 15 minutes ni descendre sous 2 minutes.
- Implémentation : helper capteur de modèle dédié calculant `EMA_ensoleillement` (en heures) à partir de l'historique `weather.beynes` et de `sun.sun` ; les seuils d'ajustement de durée sont portés par des helpers `input_number`.
### Graduation indicative de la durée selon le besoin d'arrosage
Ce paquet de règles est **indicatif** : il guide le choix de la durée en fonction du niveau de besoin d'arrosage estimé (à partir de l'EMA des températures, de la pluviométrie récente, de l'EMA d'ensoleillement…), mais reste à l'appréciation de l'agent.
| Besoin d'arrosage estimé | Durée indicative |
| ------------------------ | ------------------------------- |
| Faible | entre 1 et 5 minutes |
| Moyen | entre 5 et 10 minutes |
| Fort | entre 10 et 14 minutes (inclus) |
| Exceptionnel | 15 minutes |
- L'estimation du niveau de besoin relève du jugement de l'agent, en combinant les critères disponibles (chaleur récente et prévue, pluies récentes et prévues, ensoleillement en heures, saison).
- **Ces bornes indicatives ne dispensent pas des règles absolues 3 et 4** : concrètement, un besoin « faible » se traduit par une durée de **2 à 5 minutes** en pratique (jamais moins de 2 : sinon on n'arrose pas du tout, règle 4), et aucun niveau de besoin ne peut dépasser 15 minutes (règle 3).
- Un besoin « exceptionnel » correspond à la durée maximale de 15 minutes : très chaud, très sec et très ensoleillé, aucune pluie récente ni prévue.
### Ordre d'évaluation
Avant toute ouverture de pompe, évaluer dans cet ordre :
1. Inhibition générale et intervention manuelle éventuelles.
2. Règle 1 : pluie du jour > 2 mm → annuler.
3. Règle 2 : EMA anticipative des températures max (incluant la prévision J+1) < 20 °C → annuler.
4. Règle 5 : EMA anticipative de la pluviométrie (incluant la prévision J+1) > 2 mm → annuler.
5. Capteurs disponibles et dans une plage crédible → sinon annuler.
6. Durée prévue : entre 2 et 15 minutes inclus → sinon ajuster ou annuler (cf. règles 3 et 4).
7. Ajustement de la durée par le critère d'ensoleillement (EMA des heures d'ensoleillement) — sans jamais sortir de la fourchette 2-15 minutes (règles 3 et 4).
## 3. Comportement attendu
- Fermer la pompe d'arrosage (`switch.prise_pompe_arrosage` → `off`) à la fin du cycle, à l'expiration du minuteur et en cas d'arrêt d'urgence.
- Toujours ouvrir la pompe d'arrosage (`switch.prise_pompe_arrosage`) et la pompe de remplissage de la citerne (`switch.prise_pompe_reservoir`) en même temps.
- Après écriture d'une automatisation, vérifier les traces et l'historique des entités pour confirmer que les règles absolues ont été respectées.
Pour faire un fichier Markdown qui vous ressemble, le mieux est de le faire faire par votre LLM favoris. Donnez-lui les entités HA nécessaires et les quelques règles de contrôle absolues comme Si il a plu plus de 2 mm dans la journée, on annule l'arrosage. C'est ici que vous spécifiez vos règles et vos limites. Un soin particulier doit être apporté à ce SKILL. Procédez par petits changements et testez votre SKILL hors de Dify.
Donnez-lui aussi le SKILL associé au MCP ha-mcp. Vous le trouverez ici : https://github.com/homeassistant-ai/skills/tree/main/skills/home-assistant-best-practices (téléchargez et zippez le fichier SKILL.md et le répertoire references).
Activation des Outils (Tools)
Associez à l'agent les outils MCP nécessaires pour la collecte météo :
ha_get_state(pour lire les états des entités température, le cumul de pluie…).ha_get_history(pour lire l'historique des températures, cumul de pluie…).ha_call_service(pour pouvoir exécuter une action qui est nécessaire pour accéder aux prévisions météo).
Photo finale de l'agent « Arrosage Agent »
Vous devez obtenir un agent qui ressemble à ça :

Chapitre 6 : La configuration du pipeline complet étape par étape
Nous allons maintenant construire l'intégralité du workflow dans l'éditeur visuel de Dify. Ce pipeline s'exécute de manière séquentielle, traite les données et effectue les actions adaptées.

Allez dans Studio et créer un nouveau workflow à partir de zéro de type "Flux de travail". Nommez le "Arrosage automatique" et donnez-lui une description détaillée de ce qu'il fait. Appuyez sur "Créer" et vous arrivez sur un canevas vide.
Étape 1 : Le déclencheur (Schedule Trigger)
Le point d'entrée du workflow est un nœud Déclencheur de programmation (Schedule Trigger). Cliquez droit sur le canevas, "Ajouter un nœud", puis dans l'onglet "Démarrer", sélectionnez "Déclencheur de programmation"

- Configurez la fréquence d'exécution (ex. tous les jours à 22h00).
- Ce nœud ne nécessite aucun paramètre d'entrée spécifique.
Étape 2 : Le nœud Agent (Arrosage agent)
Vous allez ajouter un nœud de type "Agent" qui va utiliser l'agent configuré dans les étapes précédentes.
Pour cela :
- cliquez droit sur le canevas, "Ajouter un nœud" > "Agent" > sélectionnez votre agent "Arrosage agent",
- configurez un prompt pour cet agent. Sachant que tout est décrit dans le SKILL arrosage configuré ci-dessus, il n'y a pas besoin de décrire grand-chose ici. J'ai mis ça de mon côté :
Calcule la durée d'arrosage pour ce jour: {{#sys.timestamp#}}
Assures toi que l'explication est courte et contient la date du jour. Donnes la moyenne pondérée des pluies, des températures, de l'ensoleillement et ta décision uniquement. Tu as le droit au format markdown avec des retours à la ligne et des icônes pour ce champ `explication`.- reliez la sortie du nœud "Déclencheur de programmation" avec l'entrée du nœud "Agent"
Vous devriez maintenant avoir quelque-chose qui ressemble à ça :

Vous pouvez tester le bon fonctionnement de l'agent en le sélectionnant et en cliquant sur la flèche "Exécutez cette étape" en haut à droite. L'onglet "Dernière exécution" donne alors le résultat :

Tant que vous n'avez pas quelque-chose qui ressemble à ça, il faut corriger ce qui ne va pas. Vous remarquez le nombre de tokens utilisés pour cette requête (129880) dans cet exemple, on verra en fin d'article comment l'optimiser.
Étape 3 : Le nœud Code (extraction du résultat et validation)
Ce nœud est nécessaire, car la version de Dify que j'utilise ne permet d'utiliser le champarrosage_necessairequi est dans le champjson. Je n'ai pas réussi à utiliserjson.arrosage_necessairedirectement dans le noeud de test et j'ai du remettre "à plat" le résultat. Ce sera certainement corrigé dans les versions futures de Dify. Quoiqu'il en soit, c'est un bon exemple de qu'il est possible de faire, ces noeuds "Code" étant à l'usage indispensable pour faire de l'extraction / validation de données.
Cliquez droit sur le canevas, "Ajouter un nœud" > "Code" et sélectionnez le langage Python (JavaScript est aussi disponible).
Vous devez alors :
- Définir un paramètre en entrée nommé
llm_outputdans l'exemple et est mappé sur la sortie du nœud précédent. - Donner le code Python qui remet à plat la sortie du LLM (demandez à votre IA favorite de vous générer le code) :
import json
import re
def main(llm_output) -> dict:
# 1. Si Dify a déjà parsé la sortie en dictionnaire
if isinstance(llm_output, dict):
payload = llm_output.get("json", llm_output)
# 2. Si la sortie est une chaîne de caractères (texte brut)
elif isinstance(llm_output, str):
clean_output = llm_output.strip()
json_match = re.search(r'\{.*\}', clean_output, re.DOTALL)
if json_match:
clean_output = json_match.group(0)
try:
data = json.loads(clean_output)
payload = data.get("json", data) if isinstance(data, dict) else {}
except Exception:
payload = {}
else:
payload = {}
# Extraction sécurisée des champs
erreur_val = payload.get("erreur")
erreur_str = "" if erreur_val is None else str(erreur_val)
return {
"arrosage_necessaire": bool(payload.get("arrosage_necessaire", False)),
"duree_arrosage_minutes": int(payload.get("duree_arrosage_minutes", 0)),
"erreur": erreur_str,
"explication": str(payload.get("explication", ""))
}- Variables de sortie déclarées dans le nœud :
arrosage_necessaire(Boolean)duree_arrosage_minutes(Number)explication(String)erreur(String)
Votre nœud Code devrait alors ressembler à ça :

Étape 4 : Le nœud Conditionnel (SI / SINON)
Ce nœud If/Else aiguille le flux selon les données fournies par le nœud Code. Cliquez droit sur le canevas, "Ajouter un nœud" > "SI/SINON"
- Condition SI (IF) :
arrosage_necessaireestFalseOUduree_arrosage_minutes==0
- Branche SINON (ELSE) :
- S'applique quand l'arrosage doit être déclenché (
arrosage_necessaire == True).
- S'applique quand l'arrosage doit être déclenché (
Le nœud SI/SINON doit ressembler à ça :

Étape 5 : Les actions selon la branche
- le nœud "Outils / Gmail" dédié à GMail mais qui ne permet pas de faire des mails de type html plus agréable à lire,
- le nœud plus générique "Outil / email" qui est beaucoup plus simple à l'usage et permet de transformer un message écrit en Markdown en Html.
Après avoir testé les 2, j'ai finalement conservé uniquement le deuxième.
Avant de pouvoir configurer des nœuds "Email" vous devez connecter Dify à votre plate-forme d'email. Pour cela, il faut aller dans "Intégration" > "Outils" > "Tool plugin". Sélectionnez Email et suivez la procédure pour connecter Dify à votre plate-forme. Pour Gmail il vous faudra fournir une clé API.
Une fois configuré, vous devriez avoir accès au nœud "Email" en cliquant droit sur le canevas. Vous devez ajouter 2 nœuds "Email", un qui va servir pour dire "Pas d'arrosage aujourd'hui" et un autre pour annoncer le début d'arrosage avec la durée et la raison de l'allumage.
Branche SI (Pas d'arrosage nécessaire) :
- Nœud Send Email ("Pas d'arrosage") :
- Sujet :
[Arrosage] Annulé ce soir - Corps du message : Contient la variable
{{explication}}générée par le LLM et la variable{{erreur}}.
- Sujet :
Reliez directement ce nœud au nœud IF du nœud SI/SINON.
Exemple dans mon cas :

Branche SINON (Arrosage à déclencher)
- Nœud Send Email ("Arrosage prévu") :
- Sujet :
[Arrosage] Déclenchement pour {{duree_arrosage_minutes}} minutes - Corps du message : Résumé de la décision et rappel de la durée.
- Sujet :
Exemple :

Étape 6 : Nœud Requête HTTP (Déclencher l'arrosage)
Pour démarrer l'équipement sur la durée exacte calculée, nous exécutons un appel HTTP direct sur l'API REST de Home Assistant pour lancer un script (ou une automatisation) dédié. Dans mon cas, j'ai choisi un script puisque je n'ai pas besoin des triggers et conditions des automatisations. J'en profite pour positionner des input_number et input_text pour avoir la durée et les explications dans mon dashboard d'arrosage.
- Créez un nœud de "Requête HTTP" et donnez-lui cette configuration :
- Méthode :
POST - URL :
http://<IP_HOME_ASSISTANT>:8123/api/services/script/turn_on - Headers :
Authorization: Bearer <TON_JETON_LONGUE_DUREE>
- Méthode :
- Reliez le nœud HTTP au nœud Sortie.
Body (JSON) :
{
"entity_id": "script.arrosage_pompe",
"variables": {
"duree_minutes": {{duree_arrosage_minutes}},
"explication": "{{explication}}"
}
}
Mon script est alors le suivant. Vous noterez qu'il prend en paramètres la durée en minutes et les explications qui ont été calculés par le LLM. J'en profite pour envoyer des notifications avec des actions et pour vérifier que l'arrosage est bien en mode automatique :
alias: Piloter arrosage via IA
description: Pilotage complet de l'arrosage via l'IA Dify
mode: restart
fields:
duree_minutes:
description: Durée d'arrosage en minutes
example: 10
explication:
description: Raison / Explication générée par l'IA
sequence:
- action: input_number.set_value
target:
entity_id: input_number.arrosage_duree_calculee
data:
value: '{{ duree_minutes | int }}'
- action: input_text.set_value
target:
entity_id: input_text.arrosage_dernier_calcul
data:
value: >-
{{ explication | default('Avis non disponible') | string | truncate(250)
}}
- if:
- condition: select.is_option_selected
target:
entity_id: input_select.arrosage_mode
options:
behavior: any
for: '00:00:00'
option:
- Automatique
then:
- event: NOTIFIER
event_data:
action: send_to_all
title: Arrosage automatique
message: Démarrage pour {{ duree_minutes }} min. {{ explication }}
callback:
- title: Stopper arrosage
event: stopper_arrosage
image_url: /media/local/arrosage.webp
click_url: lovelace/Arrosage
icon: mdi:sprinkler
tag: arrosage
persistent: true
- action: switch.turn_on
target:
entity_id:
- switch.prise_pompe_arrosage
data: {}
- delay:
minutes: '{{ duree_minutes | int }}'
- event: NOTIFIER
event_data:
action: send_to_all
title: Arrosage automatique
message: >-
Fin de l'arrosage pendant {{
states('input_number.arrosage_duree_calculee')}} min à {{ now() }}.
callback:
- title: Rallumer
event: allumer_arrosage
image_url: /media/local/arrosage.webp
click_url: lovelace/Arrosage
icon: mdi:sprinkler
tag: arrosage
persistent: true
- action: switch.turn_off
target:
entity_id:
- switch.prise_pompe_arrosage
else:
- action: notify.persistent_notification
metadata: {}
data:
message: Pas d'arrosage via IA car le mode d'arrosage n'est pas automatique
title: Tentative d'arrosage via IA
Le flux complet
Le flux complet doit maintenant ressembler à ça :

Validation pas à pas de l'étape
Vous pouvez tester le workflow complet en cliquant sur Exécuter (Preview). Dans l'historique d'exécution :
- Vérifiez que le nœud Code extrait bien les données du JSON.
- Suivez le cheminement sur l'une des deux branches (Condition
IFouELSE). - Vérifiez la réception de l'email ainsi que la bonne exécution du script côté Home Assistant.
Chapitre 7 : Pour aller plus loin
Économiser des tokens
Au-delà de l'exemple très simple présenté dans cet article, il est possible d'avoir un cout en nombre de tokens important. Pour limiter cet effet il faut suivre quelques règles :
- Cadrer le LLM :
- Fixez lui des limites (pas plus de 15 min, pas d'arrosage s'il fait moins de 15°, ...),
- Ne laissez pas divaguer et chercher tout seul ce qui peut être précalculé : donnez lui la liste des entités concernées, dites lui comment trouver les prévisions météo, .... Sinon, il va les chercher tout seul via le MCP Home Assistant. Il va finir par les trouver, mais au prix de beaucoup d'aller retour couteux. Regardez le SKILL donné au § 2 - création de l'agent,
- Apprenez lui comment travailler efficacement via des SKILLs.
- Éviter la remontée d'historiques : les données historiques sous HA sont volumineuses. Si le LLM doit aller chercher beaucoup d'historiques sur beaucoup d'entités le volume de données est important et donc le nombre de token utilisé l'est aussi,
- Utilisez les nœuds de type code pour faire les mises en forme au lieu de demander au LLM de le faire.
Récupération d'historique via des appels d'API
Si vous avez beaucoup d'historiques d'entités à récupérer, il est conseillé de préparer ces données par des nœuds dédiés au lieu de laisser faire le LLM.
Pour cela, le nœud HTTP permet d'interroger HA via ses APIs.
Dans cet exemple, je vais interroger mon instance InfluxDB pour récupérer le maximum et la moyenne d'une liste importante d'entité - le cas d'usage est de faire une analyse énergétique.

J'utilise 3 nœuds pour faire cette récupération :
- le premier nœud de type code Python construit la requête qui sera envoyée à InfluxDB,
- le deuxième nœud de type HTTP exécute la requête via un appel HTTP,
- le troisième nœud met en forme le résultat brut de la requête pour une utilisation par l'agent
Préparation de la requête
Voici le code Python qui effectue cette préparation :
from datetime import datetime, timedelta, time
from zoneinfo import ZoneInfo
SENSORS = [
"sensor.energie_prise_tele",
"sensor.tesla_wall_connector_energy",
"sensor.energie_chargeur_garage",
"sensor.energie_poele",
"sensor.energie_prises_reservoir",
"sensor.energie_radiateur_mezzanine",
"sensor.energie_switch_chauffe_eau",
"sensor.energy_prise_deshumificateur",
"sensor.energy_prise_frigo",
"sensor.energy_prise_garage_voiture",
"sensor.energy_prise_lave_vaisselle",
"sensor.energy_radiateur_musique",
"sensor.radiateur_sam1_energy",
"sensor.energy_radiateur_sam2",
"sensor.energy_radiateur_sam3",
"sensor.energy_switch_bureau_box",
"sensor.energy_switch_four",
"sensor.energy_switch_induction",
"sensor.interrupteur_clim_chambre_energy",
"sensor.interrupteur_clim_salon_energy",
"sensor.lampe_chambre_4_energy",
"sensor.pompe_a_biere_energy",
"sensor.prise_cave_vin_energy",
"sensor.prise_congel_energy",
"sensor.prise_entree_energy",
"sensor.prise_machines_energy",
"sensor.prise_ordi_bureau_energy",
"sensor.radiateur_bureau_energy",
"sensor.radiateur_chambre_2_energy",
"sensor.radiateur_chambre_4_energy",
"sensor.radiateur_chambre_laure_energy",
"sensor.radiateur_entree_energy",
"sensor.radiateur_sdb_haut_energy",
"sensor.switch_radiateur_sdb_energy",
"sensor.energie_cuisine_prise_airfryer",
]
def main(t_start_utc: str, t_end_utc: str) -> dict:
# Dans InfluxDB, le préfixe "sensor." n'est généralement pas stocké
# dans le tag entity_id.
entity_ids = [entity_id.removeprefix("sensor.") for entity_id in SENSORS]
entity_filter = " OR ".join(
f'"entity_id" = \'{entity_id}\''
for entity_id in entity_ids
)
# SPREAD("value") calcule la consommation sur la journée.
# Le regroupement direct par entity_id conserve ce tag dans la réponse
# InfluxDB. InfluxQL 1.8 ne permet pas de filtrer directement sur le
# résultat d'une fonction d'agrégation ; le seuil doit donc être appliqué
# par l'étape qui exploite la réponse.
query = (
'SELECT SPREAD("value") AS "consumption" '
'FROM "hadb"."autogen"."kWh", "hadb"."autogen"."Wh" '
f"WHERE time >= '{t_start_utc}' AND time < '{t_end_utc}' "
f'AND ({entity_filter}) '
'GROUP BY "entity_id"'
)
return {
"query": query,
} Le résultat est une requête InfluxDB qui remonte la consommation par tranche horaire (spread). Votre IA préférée va vous faire la requête pour vous.
Exécution de la requête
Un nœud de type HTTP va exécuter la requête sur mon instance de InfluxDB. La configuration de ce nœud ressemble à ça :

INFLUX_DB, INFLUX_USER et INFLUX_PASSWORD sont des variables d'environnements. Elles sont définies en cliquant sur ce bouton en haut à droite :

Le paramètre q vient du nœud précédent de préparation de la requête.
Mise en forme du résultat
Le résultat d'exécution de la requête ressemble à ça :
{
"body": "{\"results\":[{\"statement_id\":0,\"series\":[{\"name\":\"Wh\",\"tags\":{\"entity_id\":\"chauffe_eau_total_energy\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",509]]},{\"name\":\"Wh\",\"tags\":{\"entity_id\":\"prise_tele_energy\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",72]]},{\"name\":\"Wh\",\"tags\":{\"entity_id\":\"tesla_wall_connector_energy\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"energie_chargeur_garage\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"energie_poele\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"energie_prise_machines\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"energie_prises_reservoir\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"energie_radiateur_mezzanine\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"energie_switch_chauffe_eau\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0.6399999999998727]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"energy_prise_deshumificateur\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0.049999999999954525]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"energy_prise_frigo\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",1.4700000000000273]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"energy_prise_garage_voiture\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"energy_prise_lave_vaisselle\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0.01999999999998181]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"energy_radiateur_musique\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"energy_radiateur_sam2\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"energy_radiateur_sam3\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"energy_switch_bureau_box\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0.2599999999999909]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"energy_switch_four\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"energy_switch_induction\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"interrupteur_clim_chambre_energy\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0.8999999999999773]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"interrupteur_clim_salon_energy\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0.01999999999998181]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"lampe_chambre_4_energy\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"pompe_a_biere_energy\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",1.4499999999999886]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"prise_cave_vin_energy\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0.08000000000000007]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"prise_congel_energy\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"prise_entree_energy\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"prise_machines_energy\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0.01999999999998181]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"prise_ordi_bureau_energy\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0.19999999999998863]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"radiateur_bureau_energy\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"radiateur_chambre_2_energy\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"radiateur_chambre_4_energy\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"radiateur_chambre_laure_energy\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"radiateur_entree_energy\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"radiateur_sam1_energy\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"radiateur_sdb_haut_energy\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0]]},{\"name\":\"kWh\",\"tags\":{\"entity_id\":\"switch_radiateur_sdb_energy\"},\"columns\":[\"time\",\"consumption\"],\"values\":[[\"2026-09-03T22:00:00Z\",0]]}]}]}\n",
"files": [],
"headers": {
"content-encoding": "gzip",
"content-type": "application/json",
"date": "Sat, 05 Sep 2026 08:24:29 GMT",
"request-id": "36b7a796-a903-11f1-b731-164a31a80287",
"transfer-encoding": "chunked",
"x-influxdb-build": "OSS",
"x-influxdb-version": "1.8.10",
"x-request-id": "36b7a796-a903-11f1-b731-164a31a80287"
},
"status_code": 200
}C'est indigeste et terriblement inefficace pour un LLM. Pour minimiser les coûts du LLM, il est important de mettre en forme le résultat et de filtrer les équipements qui n'ont rien consommé.
Pour cela, on utilise un nœud code qui ressemble à ça :
"""Extrait et formate la réponse InfluxDB du sous-workflow Dify équipements."""
import json
from datetime import datetime
from zoneinfo import ZoneInfo
PARIS_TIMEZONE = ZoneInfo("Europe/Paris")
LOCAL_HOURS = tuple(f"{hour:02d}h" for hour in range(24))
def parse_utc_timestamp(value: str) -> datetime:
"""Convertit un timestamp ISO 8601 UTC en objet datetime."""
return datetime.fromisoformat(value.replace("Z", "+00:00"))
def to_float(value: object) -> float | None:
"""Convertit une valeur numérique InfluxDB en flottant."""
if isinstance(value, bool) or value is None:
return None
try:
return float(value)
except (TypeError, ValueError):
return None
def format_value(value: float | None) -> str:
"""Formate une valeur en kWh dans une cellule Markdown."""
if value is None or abs(value) < 0.0005:
return ""
text = f"{round(value, 3):.3f}".rstrip("0").rstrip(".")
return text.replace(".", ",")
def main(http_response_body: str, t_start_utc: str, t_end_utc: str, significant_consumption_kwh: float) -> dict:
"""Produit le tableau horaire compact des équipements significatifs.
Entrées Dify:
http_response_body: Corps JSON retourné par le nœud HTTP InfluxDB.
t_start_utc: Début inclusif de la journée, au format UTC ISO 8601.
t_end_utc: Fin exclusive de la journée, au format UTC ISO 8601.
Sorties Dify:
formatted_response: Tableau Markdown des équipements consommant plus de 0,01 kWh.
error: Message vide lorsque le formatage a réussi.
"""
try:
start_utc = parse_utc_timestamp((t_start_utc or "").strip())
end_utc = parse_utc_timestamp((t_end_utc or "").strip())
if start_utc >= end_utc:
raise ValueError("t_start_utc doit être antérieur à t_end_utc.")
body = json.loads(http_response_body)
if not isinstance(body, dict):
raise ValueError("Le corps HTTP InfluxDB doit être un objet JSON.")
except (AttributeError, TypeError, ValueError, json.JSONDecodeError) as error:
return {
"formatted_response": "",
"error": f"Réponse ou période InfluxDB invalide : {error}",
}
rows = []
for result in body.get("results", []):
for series in result.get("series", []):
entity_id = series.get("tags", {}).get("entity_id")
columns = series.get("columns", [])
values = series.get("values", [])
if not isinstance(entity_id, str) or not isinstance(columns, list):
continue
if not isinstance(values, list) or not values:
continue
column_indexes = {column: index for index, column in enumerate(columns)}
time_index = column_indexes.get("time")
delta_index = column_indexes.get("delta")
if time_index is None or delta_index is None:
continue
hourly_values = {hour: 0.0 for hour in LOCAL_HOURS}
for value_row in values:
if not isinstance(value_row, list):
continue
if len(value_row) <= max(time_index, delta_index):
continue
try:
timestamp_utc = parse_utc_timestamp(value_row[time_index])
except (AttributeError, TypeError, ValueError):
continue
if not start_utc <= timestamp_utc < end_utc:
continue
delta_kwh = to_float(value_row[delta_index])
if delta_kwh is None:
continue
local_hour = timestamp_utc.astimezone(PARIS_TIMEZONE).strftime("%Hh")
hourly_values[local_hour] += delta_kwh
total_kwh = round(sum(hourly_values.values()), 3)
if total_kwh <= significant_consumption_kwh:
continue
rows.append(
"| "
+ entity_id
+ " | "
+ " | ".join(format_value(hourly_values[hour]) for hour in LOCAL_HOURS)
+ " |"
)
header = "| entité | " + " | ".join(LOCAL_HOURS) + " |"
separator = "|---|" + "|".join("---" for _ in LOCAL_HOURS) + "|"
return {
"eqts_formatted_response": "\n".join([header, separator, *rows]),
"error": "",
}Le résultat est alors le suivant :
{
"eqts_formatted_response": "| entité | 00h | 01h | 02h | 03h | 04h | 05h | 06h | 07h | 08h | 09h | 10h | 11h | 12h | 13h | 14h | 15h | 16h | 17h | 18h | 19h | 20h | 21h | 22h | 23h |\n|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|\n| chauffe_eau_total_energy | | | | | | | | | | | 229 | 96 | | | | | | | | | | | | |\n| prise_tele_energy | | | | | | | | | | | | | | | | | 2 | | | | 25 | 28 | | 20 |\n| energie_switch_chauffe_eau | | | | | 0,01 | | | 0,01 | | 0,04 | | 0,5 | 0,05 | | | | 0,01 | | | 0,01 | | | | 0,01 |\n| energy_prise_deshumificateur | | | | | | | 0,01 | | | | 0,01 | | | | 0,01 | | | | 0,01 | | | | 0,01 | |\n| energy_prise_frigo | 0,04 | 0,05 | 0,05 | 0,13 | 0,06 | 0,05 | 0,06 | 0,05 | 0,05 | 0,05 | 0,04 | 0,05 | 0,11 | 0,07 | 0,05 | 0,06 | 0,05 | 0,05 | 0,05 | 0,06 | 0,05 | 0,13 | 0,06 | 0,06 |\n| energy_prise_lave_vaisselle | | | | | | | | | 0,01 | | | | | | | | | 0,01 | | | | | | |\n| energy_switch_bureau_box | 0,01 | 0,02 | 0,01 | 0,01 | 0,01 | 0,01 | 0,01 | 0,01 | 0,01 | 0,01 | 0,01 | 0,01 | 0,01 | 0,02 | 0,01 | 0,01 | 0,01 | 0,01 | 0,01 | 0,01 | 0,01 | 0,01 | 0,01 | 0,02 |\n| interrupteur_clim_chambre_energy | | | 0,11 | 0,01 | 0,12 | | | | | | | | | | | | 0,01 | | | | | | 0,13 | 0,52 |\n| interrupteur_clim_salon_energy | | | | | | | | | | | | | | 0,01 | | | | | | | 0,01 | | | |\n| pompe_a_biere_energy | 0,06 | 0,09 | 0,01 | 0,11 | 0,03 | 0,04 | 0,1 | 0,04 | 0,08 | 0,06 | 0,05 | 0,04 | 0,11 | 0,02 | 0,08 | 0,07 | 0,02 | 0,11 | 0,02 | 0,08 | 0,07 | 0,02 | 0,09 | 0,06 |\n| prise_cave_vin_energy | | | | | | | | | | | 0,03 | 0,03 | | | | | | | | | | | | |\n| prise_machines_energy | | | | | | | | | | 0,01 | | | | | | | | | | 0,01 | | | | |\n| prise_ordi_bureau_energy | 0,01 | | 0,01 | 0,01 | 0,01 | 0,02 | | 0,01 | 0,01 | 0,01 | 0,01 | 0,01 | 0,01 | 0,01 | 0,01 | | 0,01 | 0,01 | 0,01 | 0,01 | 0,01 | 0,01 | 0,01 | |",
"error": ""
}Ce tableau est directement utilisable par le LLM. Seule les valeurs non vides sont présentes (économie de tokens) et la mise en forme sous forme de tableau est alors très efficace.
Chapitre 8 : Bilan et ouvertures
Les bénéfices d'une automatisation pilotée par IA
Déporter la prise de décision vers un orchestrateur comme Dify transforme la manière de concevoir sa domotique :
- Souplesse et contextualisation : Fini les scripts Jinja2 rigides et illisibles. Le LLM interprète des directives métiers rédigées en langage naturel et adapte dynamiquement sa décision à un ensemble de facteurs variables.
- Génération de comptes-rendus intelligibles : En plus de décider, l'agent explique sa décision. Vous recevez une notification claire expliquant le « pourquoi » du déclenchement ou de l'annulation.
- Maintenabilité du code Home Assistant : Vos automatisations dans Home Assistant restent très simples (de simples scripts d'action) ; toute la logique d'analyse est centralisée dans un workflow visuel clair.
Une mise en œuvre exigeante
Il est important de garder en tête le coût technique d'une telle architecture :
- Complexité d'infrastructures : Entre la gestion des conteneurs, le réseau local, le serveur MCP et l'orchestrateur Dify, la chaîne de dépendances est lourde.
- Consommation de ressources : Dify nécessite une machine ou une VM correctement dimensionnée (4 à 8 Go de RAM dédiés).
- Latence et dépendance au LLM : L'exécution d'un workflow agentique prend quelques secondes et dépend de la disponibilité de votre fournisseur de modèle (ou de votre LLM local).
Cette approche est donc à réserver à des cas d'usage à forte valeur ajoutée (gestion fine de l'arrosage, optimisation énergétique globale, surveillance contextuelle) et non pour allumer la lumière d'un couloir lors d'une détection de mouvement.
Les ouvertures : Un terrain de jeu sans limites
Une fois votre instance Dify opérationnelle et connectée à Home Assistant, les possibilités dépassent très largement le cadre de la domotique classique.
1. Des automatisations domotiques ultra-pointues
- Optimisation énergétique (EDF Tempo / Solaire) : En combinant InfluxDB (pour analyser les historiques de consommation et de production solaire) et un nœud Python dans Dify, vous pouvez planifier le démarrage de gros consommateurs (pompe à chaleur, recharge de véhicule électrique, batterie domestique) au meilleur coût financier et carbone.
- Prédictions climatiques intérieures : Ajuster la consigne des thermostats en croisant l'inertie du bâtiment, l'ensoleillement prévu et les tarifs de l'électricité.
2. Au-delà de Home Assistant
Dify est un orchestrateur généraliste. La même instance peut simultanément gérer :
- Un assistant personnel augmenté : Analyse automatique de vos e-mails ou bulletins météo pour vous envoyer un résumé quotidien sur Telegram ou Signal.
- La gestion de documents et bases de connaissances (RAG) : Interroger la documentation technique de vos appareils, vos notices de bricolage ou vos notes personnelles depuis une interface de chat unique.
- L'automatisation de workflows web : Interagir avec des API externes (GitHub, Google Workspace, services de veille) pour automatiser vos tâches du quotidien.
L'intégration de Dify avec Home Assistant n'est que la première brique d'un système d'information personnel véritablement intelligent.