Gestion de la maison automatisée par gemini

Je suis un petit nouveau sur Home Assistant, et du coup, je profite de ce post pour me présenter rapidement, car j’ai beaucoup apprécié avoir d’autres tutos comme sur frigate par exemple qui m’ont énormément servi dans ma migration.

Comme beaucoup d’autres utilisateurs, je suis un ancien de Jeedom, qui a été réellement ma première vraie solution domotique a la maison, j’avais quelques designs qui ont sauté lors d’une mise à jour, et à tout refaire, je me suis dit, autant passer sur HA directement.

Les dernières mise a jour de HA, notamment accès sur l’intelligence artificielle m’ont parue très intéressante, et j’ai commencé à m’y intéresser de plus près, il y a quelques jours.

Je suis plutôt allergique au code, et j’apprécie particulièrement Nodered dans HA, et quasiment tous mes scénarios et équipements sont déjà intégrés.

Ma maison est équipée de panneaux solaires (6 KWc), d’un pack batterie DIY de 10 KW, piloté par un onduleur SOFAR avec un pi et SOFARMQTT d’une piscine, d’une climatisation gainable avec pack AIRZONE (5 zones en tout), d’un abonnement TEMPO, pour lequel la tarification change en fonction des heures et de la couleur du jour, et nous sommes 4, deux parents et deux enfants.

Il devient vite compliqué de devoir tout gérer par des scénarios indépendants, et pour moi l’utilisation de l’intelligence artificielle prend tout son sens dans le cadre d’une gestion complète des équipements.

J’utilisais jusqu’à présent un proxy LLM couplé à un abonnement à vie chez 1MinAI, sauf que l’intégration dans HA n’est pas encore possible, car il ne prend pas en charge l’utilisation d’URL custom.

AI TASK MAANAGER :
En testant le TASK AI MANAGER intégré a HA, je me suis aperçu qu’il était possible de stocker les données de réponse dans une variable, que le LLM utilisé avait accès aux données des capteurs intégrés a HA, mais qu’il ne lui était pas possible de créer des scénarios ou d’activer des équipements (depuis le task manager).

L’idée est toute simple, fournir au LLM les données des capteurs, récupérer et stocker la réponse dans une variable, et réutiliser les éléments de réponses dans NODERED.

GEMINI :
Pour ce faire, j’ai utilisé GEMINI, qui est actuellement gratuit et vraiment permissif dans le quota de tokens journalier, et on peut l’activer facilement , il suffit de créer une clé API sur le site de google, il suffit ensuite de rentrer la clé API dans les paramètres de HA via appareils, ajouter une intégration, google, GEMINI.

Note sur GEMINI, car comme toujours avec google, il est gratuit, mais le produit c’est vous (Les données sont utilisées pour entrainer le modèle)

Il faut ensuite configurer le modèle, en augmentant la limite du nombre de tokens utilisés par HA pour chaque envoi, pour avoir accès aux paramètres, il faut décocher paramètres recommandés.

On en profite également pour aller dans paramètres assistants vocaux pour parametrer google et exposer les capteurs / équipements souhaités :

Pour faire le lien entre le AI TASK MANAGER et Nodered, il faut créer une variable txt dans appareils/entrée (ou HELPERS).

Il suffit ensuite de créer une automatisation, qui se lance pour ma part toutes les deux heures, et d’indiquer le nom de la variable dans l’onglet dédié.

LIENS AUTOMATISATION -------->NODERED :

Pour ce faire, il faut d’abord ajouter dans un flow un event state node avec pour entité notre variable, cela nous permettra de récupérer la réponse de GEMINI et de déclencher notre flow.

Dans notre automatisation, il faut ensuite rajouter une action, appelant le service Nodered compagnion en entité, on selectionne notre events state node et dans la partie message, on rentre le code suivant en mode YAML:

target:
  entity_id: switch.votre_switch
data:
  message:
    payload: "{{ votre_variable.data }}"
  output_path: "0"
action: nodered.trigger

PREMIER TEST POUR LANCER UNE COMMANDE DEPUIS LE LLM:

Pour ce faire, il faut rajouter un node debug a la suite de notre events state, et dans l’encadré prompt/instructions de AI TASK MANAGER, indiqué au LLM que c’est un test et que sa réponse doit être « Hello World », enregistrer les modifications puis lancer l’automatisation manuellement.



Si tout fonctionne correctement, vous devriez voir votre message ‹ Hello World › sur la droite onglet debug de Nodered.

Pour la suite, afin de pouvoir lancer une commande, on va indiquer au LLM le nom des commandes pour chaque équipement, comme par exemple pour une lumière le nom peut être :

light_turn_on

A la suite de notre node Events state, on ajoute un node switch que l’on va paramétrer avec une règle de valeur sur « contient » et sur l’onglet de gauche « chaine de caractères » puis mettre le nom de notre commande.

Pour finir, on rajoute a la suite un node action avec pour paramétrage le nom de l’entité/appareil correspondant a notre lampe et l’action a réaliser (turn light on).

Si tout va bien, en exécutant notre automatisation, notre lampe doit s’allumer.

A partir de la, le reste se fait dans le LLM, l’idéal étant d’avoir un client comme LIBRECHAT ou Bolt qui tourne dans un container, afin d’optimiser au mieux son prompt.

Vous trouverez ci dessous mon prompt de base et l’optimisation réalisée par GEMINI, les notions de règle et de conditions préliminaires sont importantes afin qu’il ne se plante pas, il faut également bien lui préciser de conserver les commandes et les capteurs de base, mais après quelques essais, on arrive a avoir quelque chose de plutôt fonctionnel.

je rajoute également quelques screens de l’ensemble.

TEXTE D’ORIGINE :


MISSION PRINCIPALE:
Tu dois optimiser la consommation électrique de la maison avec deux adultes et deux enfants qui sont généralement absents la journée de 8h a 18h sauf le papa qui travaille au domicile dans le bureau uniquement la semaine, tout en veillant a garantir un confort suffisant,  il faut penser également aux vacances scolaires ou les enfants ne vont pas a l'école.

L'abonnement électrique est un abonnement tempo, avec une facturation différente selon la couleur du jour et la plage horaire:
-La tarification sera toujours la moins élevée sur la plage horaire de 22h a 6h indépendamment de la couleur du jour.
-La tarification change en fonction de la couleur du jour sur la plage horaire de 6h a 22h, couleur qui peut être : bleu (le moins cher), blanc (prix moyen) et rouge (prix élevé)
-Les pics de consommation sont le midi entre 12h30 et 14h et le soir de 18h30 a 19h30 lorsque les plaques de cuisson sont utilisées.

LIMITES TECHNIQUES:
Tu n'as pas la possibilité de créer de script ou d'activer / désactiver des équipements directement dans home assistant, tu ne peux que consulter les capteurs, pour pallier a ce problème, tes réponses seront interprétées par nodered pour l'éxécution des commandes, tu seras consulter 4 fois par jour, a 6h, a 12h, a 22h, et 00h30 afin d'adapter les paramètres au mieux, ta réponse est envoyée dans une variable unique, et transférée a NODERED afin que de pouvoir activer / désactiver les équipements. Cette dernière doit être composée de la liste des commandes que tu souhaites déclencher uniquement, ces dernières sont indiquées en majuscules pour chaque équipement. A chaque consultation, tu devras donc vérifier que les données des capteurs et des commandes soient cohérentes et apporter les corrections nécessaires.

LISTE DES ÉQUIPEMENTS:
-panneaux solaires (6 kW)
-pack batterie au lithium de (10 kW) qui se recharge/décharge  en fonction de la consommation électrique totale de la maison et de l'ensoleillement (un contrôle manuel est possible : charge, décharge, ou standby)
-d'un four et d'une plaque électrique (souvent utilisés aux heures des repas)
-d'une piscine (avec traitement au sel et pompe a chaleur)
-d'une voiture électrique (qui doit se recharger souvent en cours de semaine sur une bonne de recharge)
-d'un chauffe eau thermodynamique qui fonctionne de manière autonome de 9H a 17H (consommation électrique 1.5 KW)
-d'une climatisation gainable avec 5 thermostats (Bureau, Salon, Chambre Léa, Chambre Célia, Chambre PARENTS)
-de deux cheminées au bioéthanol que les utilisateurs allument durant les journées rouges

LISTE DES CAPTEURS ET COMMANDES DISPONIBLES :

-MÉTÉO
Capteurs disponibles :
appareil: Météo-France forecast for city Vergèze - Languedoc-Roussillon (30) - FR


-TEMPO :
Capteurs disponibles:
appareil : RTE Tempo


-PANNEAUX SOLAIRES (6 kWc) :
Capteurs disponibles :
entité production: sensor.sofar_production_kw


-PACK BATTERIE AU LITHIUM (10 kW)

--capteurs disponibles :
entité SOC : sensor.sofar_battery_soc
entité statut batterie : sensor.sofar_battery_status
entité statut réseau électrique: sensor.sofar_grid_status
entité consommation électrique totale maison: sensor.sofar_grid_power
entité état actuel import/export électricité du réseau : sensor.sofar_grid_status

--Commandes disponibles :
mode auto: INVERTER_AUTO
mode décharge: INVERTER_DISCHARGE
mode charge : INVERTER_CHARGE
mode standby : INVERTER_STANDBY

---Information commandes pack batterie : 
En mode Auto, l'onduleur gère les cycles de charge/décharge afin d'utiliser au maximum la production des panneaux solaires. les modes manuels décharge, charge, et standby, sont des modes forcés généralement utilisés pendant les journées blanches ou rouges, pour forcer une charge la nuit (tarification électrique inférieure) et une décharge la journée. Le temps nécessaire pour charger les batteries est d'approximativement 5H en mode manuel, pour les journées rouges et blanches il faut donc déclencher ce mode lorsque tu es consulté aux alentours de 00h00 pour enclencher la charge et penser a remettre les batterie en auto lorsque tu es consultés aux alentours de 06h00 . Le niveau minimum de charge des batteries est de 20% (géré automatiquement par l'onduleur).


-VOITURE ÉLECTRIQUE

--Capteurs disponibles :
appareil: Volvo XC40 ELECTRIC 2023


-BORNE DE RECHARGE

--Capteurs disponibles :
appareil : LEKTRICO

--commandes disponibles :
charge on: CHARGE_ON
charge off: CHARGE_OFF

---Information commandes borne de charge :
Nous branchons la voiture a chaque fois que nous rentrons a la maison, la charge doit être déclenchée au mieux pendant les périodes de surproduction solaire, sinon la nuit a partir de 23H, en dehors des heures de pics de consommation, il faut veiller a ce qu'il y ait au minimum une capacité de batterie de la voiture de 50% ou 150Kms. La charge de la voiture est interdite pendant les périodes de tarification rouges.


-PISCINE

--Capteurs disponibles :
entité capteurs filtration:  pompe: switch.shelly1minig3_84fce6382750
entité pompe a chaleur: climate.pompe_a_chaleur

--Commandes disponibles:
filtration on: FILTRATION_ON
filtration off: FILTRATION_OFF
mode auto on (été): MODE_AUTO_ON
mode auto off (hiver): MODE_AUTO_OFF

---Information commandes piscine :
La piscine dispose déjà de deux scénarios pour la gestion des temps de filtration en hiver et en été. Temps de fonctionnement en été 10h/jour de 8h a 18h et en hiver 2h/jour de 4h a 6h, les deux modes proposés enclenchent ces scénarios, quand le mode auto est sur on, cela correspond au temps de filtration été, quand il est sur off, cela correspond au mode de filtration hiver. 


-CLIMATISATION GAINABLE :

--capteurs disponibles:
thermostat bureau appareil: Bureau
Thermostat Salon appareil: Salon
Thermostat Léa appareil: Léa
Thermostat Célia appareil: Célia
Thermostat Parents appareil: Parents

--commandes disponibles
thermostat bureau mode:
climatisation: BUREAU_MODE_CLIMATISATION
chauffage: BUREAU_MODE_CHAUFFAGE
ventilation: BUREAU_MODE_VENTILATION

températures bureau:
16: BUREAU_TEMP_16
18: BUREAU_TEMP_18
20: BUREAU_TEMP_20
22: BUREAU_TEMP_22
24: BUREAU_TEMP_24
26: BUREAU_TEMP_26

bureau on/off:
thermostat bureau on: THERMOSTAT_BUREAU_ON
thermostat bureau off: THERMOSTAT_BUREAU_OFF


températures salon:
16: SALON_TEMP_16
18: SALON_TEMP_18
20: SALON_TEMP_20
22: SALON_TEMP_22
24: SALON_TEMP_24
26: SALON_TEMP_26

salon on/off:
thermostat salon on: THERMOSTAT_SALON_ON
thermostat salon off: THERMOSTAT_SALON_OFF


températures léa
16: LEA_TEMP_16
18: LEA_TEMP_18
20: LEA_TEMP_20
22: LEA_TEMP_22
24: LEA_TEMP_24
26: LEA_TEMP_26

léa on/off:
thermostat léa on: THERMOSTAT_LEA_ON
thermostat léa off: THERMOSTAT_LEA_OFF


températures célia
16: CELIA_TEMP_16
18: CELIA_TEMP_18
20: CELIA_TEMP_20
22: CELIA_TEMP_22
24: CELIA_TEMP_24
26: CELIA_TEMP_26

célia on/off:
thermostat célia on: THERMOSTAT_CELIA_ON
thermostat célia off: THERMOSTAT_CELIA_OFF


températures parents
16: PARENTS_TEMP_16
18: PARENTS_TEMP_18
20: PARENTS_TEMP_20
22: PARENTS_TEMP_22
24: PARENTS_TEMP_24
26: PARENTS_TEMP_26

parents on/off:
thermostat parents on: THERMOSTAT_PARENTS_ON
thermostat parents off: THERMOSTAT_PARENTS_OFF

---Information commandes climatisation gainable : Le mode de fonctionnement de la climatisation gainable est géré par le thermostat maitre, celui du bureau, le mode peut être climatisation, chauffage ou ventilation ce dernier sera appliqué a l'ensemble des thermostats. Le système installé est une climatisation gainable MITSUBISHI de 12KW avec système AIRZONE, permettant de gérer la température dans chaque pièces indépendamment. La gestion des températures actuelle en été est  24° en journée et 26° la nuit, pour l'hiver la température est de  20° la journée et 18° la nuit).


-VMC DOUBLE FLUX

--capteurs disponibles :
vmc température extérieure insufflation entité: sensor.vmc_temperature_ext_ins
vmc température extérieure entité: sensor.vmc_temperature_ext
vmc saison détectée entité: sensor.vmc_saison_detectee

--commandes disponibles :
mode boost: entité: VMC_MODE_BOOST
mode normal: VMC_MODE_NORMAL
mode vacances: VMC_MODE_VACANCES

ALARME

PRESENCE DES OCCUPANTS

--capteurs

-SCENARIO PRÉÉXISTANT JOURNÉES ROUGES
un scénario existe déja pour charger le pack batterie lors des journées rouges, et lancer le déchargement pendant la journée, ce dernier coupe également les périphériques pour économiser de l'énergie, vérifie lors de ton lancement a 00h30 que le pack batterie soit bien en charge

EXEMPLE D'ACTIONS POUVANT ETRE RÉALISÉES :

-lorsque une prévision météo prévoit des nuages ou de la pluie (sans soleil), lancer la charge complète du pack batterie la veille pendant la nuit pour déchargement la journée
-déclencher le scénario économie d'énergie qui coupe la piscine, la charge de la voiture et les appareils électriques inutilisés et le chauffage pendant la plage horaire des journées rouges
-définir le mode de fonctionnement de la piscine (été ou hiver) en fonction de la saison
-gérer le rechargement de la voiture électrique en fonction de l'état de recharge du pack batterie (si 100%, ok pour la charge de la voiture en journée durant les journées ensoleillés), sinon privilégier la recharge nuit
-réaliser des actions qui te semble juste

Pour l'instant aucun capteur n'est utilisé, nous sommes dans un environnement de simulation, donne moi les actions que tu pourrais réaliser aujourd'hui en te basant sur des valeurs météos et de présence de simulation, vérifie que l'ensemble des capteurs et actionneurs soient accessibles et  liste moi les valeurs réelles auxquelles tu as accès


PROMPT OPTIMISÉ GEMINI :


MISSION PRINCIPALE  
Tu es un automate de gestion d'énergie. Ton unique objectif est d'exécuter les règles suivantes pour minimiser la facture d'électricité et maintenir le confort. Tu ne dois pas dévier de ces règles.

CONTEXTE DE LA MAISON  
Famille : 2 adultes, 2 enfants.  
Occupation Semaine : Père en télétravail au Bureau (8h-18h), mère présente pour le déjeuner (12h30-14h).  
Occupation Week-end/Vacances : Famille présente.  
Contrat Électrique : Tempo. Heures Creuses 22h-6h. Heures Pleines 6h-22h (Bleu, Blanc, Rouge).  
Pics de consommation à éviter : 12h30-14h et 18h30-19h30.

LIMITES TECHNIQUES ET FORMAT DE RÉPONSE  
Tu es consulté à des moments clés de la journée (autour de 00:30, 06:00, 08:00, 12:00, 14:00, 18:00, 20:00, 22:00). Ta logique doit être flexible à une variation de +/- 15 minutes.  
Réponse : Ta réponse doit être UNIQUEMENT une liste de commandes en MAJUSCULES, une par ligne. Aucun texte, commentaire ou explication n'est autorisé.

--- ÉQUIPEMENTS, CAPTEURS ET COMMANDES DISPONIBLES ---

MÉTÉO ET TEMPO  
Capteurs : Appareil Météo-France forecast for city Vergèze, Appareil RTE Tempo.

PANNEAUX SOLAIRES (6 kWc)  
Capteurs : sensor.sofar_production_kw.

PACK BATTERIE LITHIUM (10 kW)  
Capteurs : sensor.sofar_battery_soc, sensor.sofar_battery_status, sensor.sofar_grid_power, sensor.sofar_grid_status.  
Commandes : INVERTER_AUTO, INVERTER_CHARGE, INVERTER_DISCHARGE, INVERTER_STANDBY.

VOITURE ÉLECTRIQUE ET BORNE DE RECHARGE  
Capteurs : Appareil Volvo XC40 ELECTRIC 2023, Appareil LEKTRICO.  
Commandes : CHARGE_ON, CHARGE_OFF.

PISCINE  
Capteurs : switch.shelly1minig3_84fce6382750 (filtration), climate.pompe_a_chaleur.  
Commandes : MODE_AUTO_ON, MODE_AUTO_OFF.

CLIMATISATION GAINABLE  
Capteurs : Appareil Bureau, Appareil Salon, Appareil Cuisine, Appareil Chambre Parents, Appareil Chambre Enfants.  
Commandes :  
Modes généraux (appliqués via Bureau) : BUREAU_MODE_CLIMATISATION, BUREAU_MODE_CHAUFFAGE, BUREAU_MODE_VENTILATION  
Commandes par pièce :  
BUREAU : THERMOSTAT_BUREAU_ON, THERMOSTAT_BUREAU_OFF, BUREAU_TEMP_16, BUREAU_TEMP_18, BUREAU_TEMP_20, BUREAU_TEMP_22, BUREAU_TEMP_24, BUREAU_TEMP_26  
SALON : THERMOSTAT_SALON_ON, THERMOSTAT_SALON_OFF, SALON_TEMP_16, SALON_TEMP_18, SALON_TEMP_20, SALON_TEMP_22, SALON_TEMP_24, SALON_TEMP_26  
CUISINE : THERMOSTAT_CUISINE_ON, THERMOSTAT_CUISINE_OFF, CUISINE_TEMP_16, CUISINE_TEMP_18, CUISINE_TEMP_20, CUISINE_TEMP_22, CUISINE_TEMP_24, CUISINE_TEMP_26  
CHAMBRE_PARENTS : THERMOSTAT_CHAMBRE_PARENTS_ON, THERMOSTAT_CHAMBRE_PARENTS_OFF, CHAMBRE_PARENTS_TEMP_16, CHAMBRE_PARENTS_TEMP_18, CHAMBRE_PARENTS_TEMP_20, CHAMBRE_PARENTS_TEMP_22, CHAMBRE_PARENTS_TEMP_24, CHAMBRE_PARENTS_TEMP_26  
CHAMBRE_ENFANTS : THERMOSTAT_CHAMBRE_ENFANTS_ON, THERMOSTAT_CHAMBRE_ENFANTS_OFF, CHAMBRE_ENFANTS_TEMP_16, CHAMBRE_ENFANTS_TEMP_18, CHAMBRE_ENFANTS_TEMP_20, CHAMBRE_ENFANTS_TEMP_22, CHAMBRE_ENFANTS_TEMP_24, CHAMBRE_ENFANTS_TEMP_26

VMC DOUBLE FLUX  
Capteurs : sensor.vmc_temperature_ext_ins, sensor.vmc_temperature_ext, sensor.vmc_saison_detectee.  
Commandes : VMC_MODE_BOOST, VMC_MODE_NORMAL, VMC_MODE_VACANCES.

PRESENCE DES OCCUPANTS  
Capteurs : device_tracker.v2337a (Père), device_tracker.pixel_8a (Mère).

--- INSTRUCTIONS DÉTAILLÉES ET RÈLES D'ACTION ---

RÈGLE MAJEURE : GESTION DES JOURS ROUGES  
Si le jour actuel est un jour Rouge, la logique dépend de l'heure de la consultation :  
Pendant les heures pleines (entre 06:00 et 22:00) : ignore toutes les autres instructions et ne renvoie AUCUNE commande. Ta réponse doit être vide pour ne pas interférer avec le scénario d'économie d'énergie.  
Pendant les heures creuses (entre 22:00 et 06:00) : suis la "PROCÉDURE NORMALE" ci-dessous, mais en appliquant une règle prioritaire pour la batterie : à la consultation de 00:30, la commande INVERTER_CHARGE doit toujours être envoyée, quelle que soit la météo.

PROCÉDURE NORMALE (POUR LES JOURS BLEUS, BLANCS, ET LES HEURES CREUSES DES JOURS ROUGES)

PROCÉDURE PRÉLIMINAIRE 1 : DÉTERMINATION DU MODE SAISONNIER  
Définis le mode saisonnier de la maison en analysant les prévisions météo sur 5 jours : ÉTÉ (>22°C), HIVER (<15°C), ou MI-SAISON.

PROCÉDURE PRÉLIMINAIRE 2 : DÉTERMINATION DE LA MÉTÉO DU JOUR  
Évalue la prévision météo pour la journée actuelle : MAUVAISE ('Pluie', 'Nuageux', 'Couvert') ou BONNE.

PROCÉDURE PRÉLIMINAIRE 3 : DÉTERMINATION DE LA PRÉSENCE  
Détermine la présence des adultes :  
PRESENCE_PERE : VRAI si device_tracker.v2337a est 'home' ou 'active'. FAUX sinon.  
PRESENCE_MERE : VRAI si device_tracker.pixel_8a est 'home' ou 'active'. FAUX sinon.  
MAISON_INHABITEE : VRAI si PRESENCE_PERE est FAUX ET PRESENCE_MERE est FAUX. FAUX sinon.

PACK BATTERIE LITHIUM

1. Lors de la consultation de DÉBUT DE JOURNÉE (autour de 00:30) : Si le jour est Bleu ou Blanc ET que la météo du jour est MAUVAISE, envoie INVERTER_CHARGE. (La règle pour le jour Rouge est gérée par la Règle Majeure).
2. Lors de la consultation de FIN DE NUIT (autour de 06:00) : Si la batterie était en charge forcée, envoie INVERTER_AUTO.
3. Règle par défaut : Aux autres consultations, envoie INVERTER_AUTO.

VOITURE ÉLECTRIQUE

1. Si la batterie de la voiture est au-dessus de 50% (ou 150 km), envoie CHARGE_OFF.
2. Si la charge est nécessaire :  
    En cas de surplus solaire (batterie maison >95%), envoie CHARGE_ON.  
    Sinon, envoie CHARGE_ON uniquement pendant les heures creuses (22h00-06h00).
3. Règle par défaut : Dans tous les autres cas, envoie CHARGE_OFF.

PISCINE  
Si le mode est ÉTÉ, envoie MODE_AUTO_ON.  
Si le mode est HIVER ou MI-SAISON, envoie MODE_AUTO_OFF.

CLIMATISATION GAINABLE

1. Définis le mode général (CHAUFFAGE, CLIMATISATION, VENTILATION) en fonction du mode saisonnier.
2. Si MAISON_INHABITEE est VRAI :  
    Si le mode est HIVER : Pour chaque pièce (BUREAU, SALON, CUISINE, CHAMBRE_PARENTS, CHAMBRE_ENFANTS), envoie THERMOSTAT_[PIECE]_ON et [PIECE]_TEMP_16.  
    Si le mode est ÉTÉ : Pour chaque pièce (BUREAU, SALON, CUISINE, CHAMBRE_PARENTS, CHAMBRE_ENFANTS), envoie THERMOSTAT_[PIECE]_ON et [PIECE]_TEMP_26.  
    Si le mode est MI-SAISON : Pour chaque pièce (BUREAU, SALON, CUISINE, CHAMBRE_PARENTS, CHAMBRE_ENFANTS), envoie THERMOSTAT_[PIECE]_OFF.  
    Ignore les règles suivantes de cette section.
3. Si le mode est MI-SAISON (et MAISON_INHABITEE est FAUX), désactive tous les thermostats.
4. Si le mode est HIVER ou ÉTÉ (et MAISON_INHABITEE est FAUX), définis l'état de chaque thermostat en fonction de l'heure actuelle :  
    Plage 22:00-08:00 (NUIT) : Active les chambres (température nuit : 18°C Hiver / 26°C Été). Désactive les pièces de vie.  
    Plage 08:00-22:00 (JOUR SEMAINE) : Active le BUREAU (température jour : 20°C Hiver / 24°C Été). Active SALON/CUISINE seulement entre 12:30-14:00 et 18:00-22:00 (température jour). Active les chambres en pré-confort (température jour) entre 20:00 et 22:00.  
    Plage 08:00-22:00 (JOUR WEEK-END/VACANCES) : Active les pièces de vie (température jour). Active les chambres en pré-confort (température jour) entre 20:00 et 22:00.  
    Pour chaque pièce, si l'heure actuelle n'est pas dans sa plage d'activité, désactive son thermostat.

VMC DOUBLE FLUX  
Si MAISON_INHABITEE est VRAI, envoie VMC_MODE_VACANCES.  
Sinon, envoie VMC_MODE_NORMAL.


FLOW FINAL:

RÉPONSE GEMINI FINALE:

Voila, j’espere que ma petite contribution permettra d’aider quelques personnes, pour ma part, c’est exactement ce que je voulais, il y a certainement de meilleures facons de faire, mais pour l’instant cette solution fonctionne plutot bien.

2 « J'aime »

Bonjour @Necti_Home

Merci pour la présentation et pour la démo impliquant gemini

1 « J'aime »

Salut @Necti_Home
Merci pour le partage.
Sois le bienvenu sur HACF :wink:
@+ Guy

Bonjour a tous,

Petit retour d’expérience après quelques jours,

Il s’avère que ma méthode fonctionnera avec des sensors ayant des valeurs fixes pour la journée comme par exemple la météo ou la couleur du jour, car ces derniers sont actualisés avec une certaine latence, pouvant aller jusqu’a plusieurs heures, je ne m’explique pas pourquoi.

J’ai réalisé une refonte du projet sur nodered, a l’aide de gemini via des scripts et des noeuds fonctions, l’idée de base étant de toujours consulter le LLM a deux étapes cruciales, la définition de la météo et la rédaction du rapport.

Plusieurs sécurités ont été ajoutées, comme une tempo de deux minutes utilisant des capteurs locaux en cas de défaillance de gemini.

Il y a également l’envoi de deux commande a 06h et a 22h pour la coupure et l’activation des équipements non essentiels lors des jours rouges.

Etc…

DETECTION DE PRESENCE:

La détection de présence est réalisée a l’aide de NUTS bluetooth que l’on a sur nos clés, et que je fais tourner sur mes modules volets roulants SHELLY, pour ce faire, il vous suffit de personaliser les adresses MAC dans le script ci dessous, ca fonctionne avec les NUTS ou TILE et c’est hyper réactif (prise en compte de la présence en moins de 10 secondes).

let origine = Shelly.getDeviceInfo().name;
let genericTopic = "shellies/script/scanNut/";
let nutsShelly = []; // Nuts vus par ce Shelly
let nutsBroker = []; // Nuts vus par le broker
let nutsName = {
  "XX:XX:XX:XX:XX:XX": "NUT NOIR",
  "XX:XX:XX:XX:XX:XX": "JENNY",
  "XX:XX:XX:XX:XX:XX": "YOHAN"
};

// Construction des listes pour les adresses MAC et leurs noms
let macList = [];
let nameList = [];
for (let mac in nutsName) {
  macList.push(mac);
  nameList.push(nutsName[mac]);
}

if (MQTT.isConnected()) {
  MQTT.publish(genericTopic + origine + '/nombreNut', "0", 0, false);
  MQTT.publish(genericTopic + origine + '/nameNut', "Vide", 0, false);
  MQTT.publish(genericTopic + origine + '/missingNut', "Vide", 0, false);
}

function isMacValid(mac) {
  if (mac.length !== 17) {
    return false;
  }
  for (let i = 0; i < 17; i++) {
    if (i % 3 === 2) {
      if (mac.charCodeAt(i) !== 0x3a) { // 0x3a = :
        return false;
      }
    } else {
      if (mac.charCodeAt(i) < 0x30 || (mac.charCodeAt(i) > 0x39 && mac.charCodeAt(i) < 0x61) || mac.charCodeAt(i) > 0x66) {
        return false;
      }
    }
  }
  return true;
}

// Fonction de tri manuelle
function sortArray(arr, order) {
  for (let i = 0; i < arr.length - 1; i++) {
    for (let j = 0; j < arr.length - i - 1; j++) {
      // Tri ascendant ou descendant
      if ((order === "asc" && arr[j] > arr[j + 1]) || (order === "dsc" && arr[j] < arr[j + 1])) {
        let temp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = temp;
      }
    }
  }
  return arr;
}

function nutList(nuts, nutsName) {
  let nutsList = [];
  for (let i = 0; i < nuts.length; i++) {
    nutsList.push(nutsName[nuts[i].mac]);
  }
  return sortArray(nutsList, "asc");
}

function nutAjout(mac, unixtime, nuts) {
  let existePas = true;
  for (let i = 0; i < nuts.length; i++) {
    if (nuts[i].mac === mac) {
      nuts[i].timePublish = unixtime;
      existePas = false;
      break;
    }
  }
  if (existePas && isMacValid(mac)) {
    nuts.push({ mac: mac, timePublish: unixtime });
  }
}

MQTT.subscribe(
  genericTopic + "#",
  function (topic, message, callback) {
    let endTopic = topic.slice(callback[0].length + 18);
    
    // Pour traiter uniquement les adresses MAC définies dans nutsName
    let mac = topic.slice(callback[0].length, callback[0].length + 17);
    if (macList.indexOf(mac) !== -1 && endTopic === "unixtime") {
      nutAjout(mac, JSON.parse(message), callback[1]);
    }
  },
  [genericTopic, nutsBroker]
);

Timer.set(
  10000, // 10s
  true,
  function (callback) {
    for (let i = 0; i < callback[1].length; i++) {
      if (callback[1][i].timePublish + 110 <= Shelly.getComponentStatus("sys").unixtime) {
        if (MQTT.isConnected()) {
          MQTT.publish(callback[0] + callback[1][i].mac + '/presence', "0", 0, false);
        }
      }
    }

    for (let i = callback[3].length - 1; i >= 0; i--) {
      if (callback[3][i].timePublish + 110 <= Shelly.getComponentStatus("sys").unixtime) {
        callback[3].splice(i, 1);
        let aList = nutList(callback[3], callback[4]);
        let aMissing = nameList.filter(function(nut) { return aList.indexOf(nut) === -1; });
        aMissing = sortArray(aMissing, "asc");
        let missing = aMissing.join('--') || "Vide";

        if (MQTT.isConnected()) {
          MQTT.publish(callback[0] + callback[2] + '/nombreNut', JSON.stringify(callback[3].length), 0, false);
          MQTT.publish(callback[0] + callback[2] + '/nameNut', aList.join('--'), 0, false);
          MQTT.publish(callback[0] + callback[2] + '/missingNut', missing, 0, false);
        }
      }
    }
  },
  [genericTopic, nutsBroker, origine, nutsShelly, nutsName]
);

function scanCB(ev, res, callback) {
  if (ev === BLE.Scanner.SCAN_RESULT) {
    if (macList.indexOf(res.addr) !== -1) {
      let mac = res.addr;
      let topic = callback[0] + mac + '/';
      let unixtime = Shelly.getComponentStatus("sys").unixtime;
      nutAjout(mac, unixtime, callback[1]);

      // Utilisation de aList ici
      let aList = nutList(callback[1], callback[3]);
      let aMissing = nameList.filter(function(nut) { return aList.indexOf(nut) === -1; });
      aMissing = sortArray(aMissing, "asc");
      let missing = aMissing.join('--') || "Vide";
        
      if (MQTT.isConnected()) {
        MQTT.publish(topic + 'rssi', JSON.stringify(res.rssi), 0, false);
        MQTT.publish(topic + 'unixtime', JSON.stringify(unixtime), 0, false);
        MQTT.publish(topic + 'presence', "1", 0, false);
        MQTT.publish(topic + 'origine', JSON.stringify(callback[2]), 0, false);
        MQTT.publish(callback[0] + callback[2] + '/nombreNut', JSON.stringify(callback[1].length), 0, false);
        MQTT.publish(callback[0] + callback[2] + '/nameNut', aList.join('--'), 0, false);
        MQTT.publish(callback[0] + callback[2] + '/missingNut', missing, 0, false);
      }
    }
  }
}

BLE.Scanner.Start({ duration_ms: -1 }, scanCB, [genericTopic, nutsShelly, origine, nutsName]);

Des helpers, permettent :
-d’activer ou non le préchauffage matin et préchauffage soir,
-de définir les températures de consignes pour le mode économique, confort,etc…
-d’activer une gestion dynamique de la température en fonction de la température extérieure (et de définir les seuils de ces dernieres).
-de définir les seuils de charges de la batterie de la voiture
-de définir les seuils de charge du pack batterie de la maison
-de définir un seuil pour la variable « surplus solaire »
-d’activer une génération du rapport a chaque éxécution (debbug) ou une fois par jour
-de définir l’heure de génération du rapport
Etc…

C’est hyper complet.

Vous trouverez ci-dessous le projet mis à jour, avec le flow nodered a importer, c’est trés robuste, et ca fonctionne super bien.

Il vous faudra installer la palette « Noeud GEMINI » dans Nodered

Il vous sera très facile de l’importer et de le modifier en fonction du nom de vos capteurs et sensors dans GEMINI ou CHATGPT par exemple.

J’y ai passé quelques heures en debbogage, pour vous faciliter la tache, vous trouverez ci-dessous le projet intégral.

Dans la pratique, j’ai utilisé OBSIDIAN, avec le plugin COPILOT et gémini de paramétrer a l’intérieur, cela permet de partager des notes et des images avec le LLM, c’est trés pratique et les versions gratuites sont hyper completes.

1. Objectif Général

Ce flux automatise la gestion énergétique et le confort de la maison en se basant sur la saison, l’heure, la présence des occupants, le tarif Tempo et la météo.

Le cœur du système est une logique de température dynamique qui ajuste intelligemment les consignes de confort en fonction de la production solaire, de la consommation électrique et des températures extérieures extrêmes pour optimiser les coûts et le confort.

Chaque jour, le flux génère des commandes pour Home Assistant et produit un rapport quotidien fiable et synthétique via l’API Gemini, qui inclut une touche de personnalisation (blague ou citation).

2. Acteurs Principaux

  • Home Assistant : Fournit les états des capteurs, les réglages (helpers) et exécute les commandes finales.
  • Node-RED : Contient toute la logique de décision, répartie en plusieurs nœuds spécialisés.
  • Gemini (Google AI) : Utilisé pour deux tâches distinctes :
    1. Une analyse météo simplifiée pour obtenir la température maximale prévue.
    2. La rédaction du rapport quotidien formaté, en se basant sur des instructions très précises.

3. Liste des Helpers Requis

Tous ces helpers doivent être créés dans Home Assistant pour que le flux fonctionne.

  • Températures :
    • input_number.temp_confort_hiver (ex: 20)
    • input_number.temp_eco_nuit (ex: 18)
    • input_number.temp_confort_ete (ex: 24)
    • input_number.temp_hors_gel (ex: 16)
  • Horaires :
    • input_boolean.activation_prechauffage_matin (toggle)
    • input_number.heure_debut_prechauffage_matin (ex: 5)
    • input_number.heure_fin_prechauffage_matin (ex: 7)
    • input_boolean.activation_prechauffage_soir (toggle)
    • input_number.heure_debut_prechauffage_soir (ex: 20)
    • input_number.heure_fin_prechauffage_soir (ex: 22)
    • input_boolean.ventilation_auto_mi_saison (toggle)
    • input_number.heure_debut_ventilation_mi_saison (ex: 10)
    • input_number.heure_fin_ventilation_mi_saison (ex: 14)
  • Voiture Électrique :
    • input_number.seuil_surplus_solaire_voiture (ex: 1000)
    • input_number.seuil_batterie_voiture_max (ex: 90)
  • Gestion Dynamique :
    • input_number.ajustement_temp_dynamique (ex: 1.0)
    • input_number.seuil_modulation_eco (ex: -2000)
    • input_number.seuil_boost_solaire (ex: 1500)
    • input_number.seuil_froid_exterieur (ex: 0)
    • input_number.seuil_chaud_exterieur (ex: 35)
  • Général / Forçage :
    • input_number.duree_forcage_manuel (en heures, ex: 3)
    • input_number.heure_rapport_quotidien (ex: 12)
    • input_number.minute_rapport_quotidien (ex: 30)
    • input_boolean.gestion_auto_bureau
    • input_boolean.gestion_auto_salon
    • input_boolean.gestion_auto_ch_parents
    • input_boolean.gestion_auto_ch_lea
    • input_boolean.gestion_auto_ch_celia
    • input_boolean.rapports_maison (mode debug pour forcer le rapport)

4. Déroulement du Flux (Mis à Jour)

  1. Récupération Météo : Un premier appel à Gemini récupère les prévisions météo de base (température max).
  2. Nœud « Variables de Contexte » : Ce nœud « Fonction » collecte l’état de tous les capteurs et helpers de Home Assistant en une seule fois pour préparer les données.
  3. Nœud « Logique Principale » : Le cerveau du flux. Ce nœud « Fonction » exécute toute la logique de décision (chauffage, voiture, batterie…). Il génère un « bulletin de décision » interne pour le rapport et possède deux sorties :
    • Sortie 1 (Actions) : Envoie un message contenant la liste des commandes (msg.commands) à exécuter immédiatement par Home Assistant.
    • Sortie 2 (Rapport) : Si l’heure du rapport est arrivée (ou si le mode debug est activé), envoie un message complet contenant toutes les données ET la liste des commandes (msg.payload et msg.commands) vers la branche de génération du rapport.
  4. Nœud « Génération du Prompt Gemini » : Ce nœud « Fonction » reçoit les informations de la Sortie 2 et construit un prompt textuel très détaillé et directif pour Gemini.
  5. Génération du Rapport : Un second appel à Gemini utilise ce prompt pour rédiger le rapport final, formaté comme demandé (regroupement des pièces, blague/citation).
  6. Notification : Le rapport final est envoyé.

5. Liste des Commandes Générées

Voici la liste complète des commandes textuelles générées par le nœud « Logique Principale » et interprétées par les nœuds suivants.

  • Thermostats :
    • THERMOSTAT_{PIECE}_ON / THERMOSTAT_{PIECE}_OFF
    • {PIECE}_TEMP_{temperature} (ex: SALON_TEMP_20.5)
    • {PIECE}_MODE_CHAUFFAGE / ..._CLIMATISATION / ..._VENTILATION
  • Voiture Électrique :
    • CHARGE_VOITURE_ON / CHARGE_VOITURE_OFF
  • Batterie Maison :
    • PACK_BATTERIE_CHARGE / PACK_BATTERIE_AUTO / PACK_BATTERIE_STANDBY
  • Piscine :
    • MODE_AUTO_PISCINE_ON / MODE_AUTO_PISCINE_OFF
  • VMC :
    • VMC_MODE_NORMAL / VMC_MODE_VACANCES
  • Gestion Jours Rouges :
    • ECONOMIE_ECONOMIE_ON (envoyée à 6h)
    • ECONOMIE_ECONOMIE_OFF (envoyée à 22h)
  • Notification :
    • NOTIFIER_ERREUR_METEO

Variables de Contexte (Internes à Node-RED)

Ces variables sont collectées et utilisées par les nœuds « Fonction » pour prendre des décisions.

  • MODE_SAISONNIER
  • MAISON_INHABITEE
  • COULEUR_TEMPO
  • METEO_JOUR
  • HEURE / MINUTE
  • SURPLUS_PUISSANCE
  • NIVEAU_BATTERIE_MAISON
  • NIVEAU_BATTERIE_VOITURE
  • TEMP_EXTERIEURE_ACTUELLE
  • CONSIGNES_ACTUELLES (objet avec les températures de chaque pièce)
  • ETATS_CLIM (objet avec l’état de chaque thermostat)
  • GEMINI_FAILED (booléen)
  • RAPPORT_DEBUG_MODE (booléen)
  • RAPPORT_AJUSTEMENT_TEMP (pour le rapport)
  • RAPPORT_ACTION_BATTERIE (pour le rapport)
  • SENSOR_ERRORS (liste des capteurs en erreur)

CODE DU NŒUD : VARIABLES DE CONTEXTE (Fonction)

// --- Début du script ---
const states = global.get('homeassistant.homeAssistant.states');
const now = new Date();
const currentMonth = now.getMonth() + 1; // +1 car les mois vont de 0 à 11

let new_payload = {};
let SENSOR_ERRORS = [];

// --- Fonctions sécurisées ---
function getState(entityId, sensorName, defaultValue) {
    const entity = states[entityId];
    if (!entity || entity.state === 'unavailable' || entity.state === 'unknown') {
        SENSOR_ERRORS.push(sensorName);
        return defaultValue;
    }
    return entity.state;
}

function getNumericState(entityId, sensorName, defaultValue) {
    const state = getState(entityId, sensorName, defaultValue.toString());
    const value = parseFloat(state);
    if (isNaN(value)) {
        if (!SENSOR_ERRORS.includes(sensorName)) { SENSOR_ERRORS.push(sensorName); }
        return defaultValue;
    }
    return value;
}

function getNumericAttribute(entityId, attributeName, sensorName, defaultValue) {
    const entity = states[entityId];
    if (!entity || !entity.attributes) {
        SENSOR_ERRORS.push(sensorName);
        return defaultValue;
    }
    if (!entity.attributes.hasOwnProperty(attributeName) || entity.attributes[attributeName] === null) {
        return defaultValue;
    }
    const value = parseFloat(entity.attributes[attributeName]);
    if (isNaN(value)) {
        if (!SENSOR_ERRORS.includes(sensorName)) { SENSOR_ERRORS.push(sensorName); }
        return defaultValue;
    }
    return value;
}

// --- 1. LOGIQUE D'ANALYSE MÉTÉO SIMPLIFIÉE ---
const geminiResponse = msg.payload;
new_payload.GEMINI_FAILED = msg.gemini_failed || false;
const tempMax = geminiResponse.temp_max;
new_payload.METEO_JOUR = geminiResponse.meteo_jour || 'MAUVAISE';

// --- LOGIQUE DE SAISON 100% FIABLE (MISE À JOUR) ---
if (tempMax !== null) {
    if (tempMax < 14) { new_payload.MODE_SAISONNIER = 'HIVER'; } 
    else if (tempMax > 23) { new_payload.MODE_SAISONNIER = 'ÉTÉ'; }
    else {
        if ([12, 1, 2].includes(currentMonth)) { new_payload.MODE_SAISONNIER = 'HIVER'; }
        else if ([3, 4, 5, 6].includes(currentMonth)) { new_payload.MODE_SAISONNIER = 'MI-SAISON-ETE'; }
        else if ([7, 8].includes(currentMonth)) { new_payload.MODE_SAISONNIER = 'ÉTÉ'; }
        else { new_payload.MODE_SAISONNIER = 'MI-SAISON-HIVER'; }
    }
} else {
    node.warn("Température max indisponible. Définition de la saison par le mois uniquement.");
    if ([12, 1, 2].includes(currentMonth)) { new_payload.MODE_SAISONNIER = 'HIVER'; }
    else if ([6, 7, 8].includes(currentMonth)) { new_payload.MODE_SAISONNIER = 'ÉTÉ'; }
    else if ([3, 4, 5].includes(currentMonth)) { new_payload.MODE_SAISONNIER = 'MI-SAISON-ETE'; }
    else { new_payload.MODE_SAISONNIER = 'MI-SAISON-HIVER'; }
}

// --- 2. Logique de présence ---
const presenceState = getState('sensor.presence_sensor', 'Capteur de présence', 'Vide');
const alarmeState = getState('alarm_control_panel.alarmo', 'Alarme', 'disarmed');
const alarmeActivee = alarmeState !== 'disarmed';
new_payload.MAISON_INHABITEE = (presenceState === 'Vide') || alarmeActivee;

// --- 3. Récupération des états et consignes des thermostats ---
new_payload.CONSIGNES_ACTUELLES = {
    'BUREAU': getNumericAttribute('climate.bureau', 'temperature', 'Consigne Bureau', 0),
    'SALON': getNumericAttribute('climate.salon', 'temperature', 'Consigne Salon', 0),
    'CHAMBRE_PARENTS': getNumericAttribute('climate.parents', 'temperature', 'Consigne Chambre Parents', 0),
    'CHAMBRE_LEA': getNumericAttribute('climate.lea', 'temperature', 'Consigne Chambre Léa', 0),
    'CHAMBRE_CELIA': getNumericAttribute('climate.celia', 'temperature', 'Consigne Chambre Célia', 0)
};
new_payload.ETATS_CLIM = {
    'BUREAU': getState('climate.bureau', 'État Bureau', 'off'),
    'SALON': getState('climate.salon', 'État Salon', 'off'),
    'CHAMBRE_PARENTS': getState('climate.parents', 'État Chambre Parents', 'off'),
    'CHAMBRE_LEA': getState('climate.lea', 'État Chambre Léa', 'off'),
    'CHAMBRE_CELIA': getState('climate.celia', 'État Chambre Célia', 'off')
};

// --- 4. Récupération des Paramètres (Helpers) ---
// Températures
new_payload.TEMP_CONFORT_HIVER = getNumericState('input_number.temp_confort_hiver', 'Temp Confort Hiver', 20);
new_payload.TEMP_ECO_NUIT = getNumericState('input_number.temp_eco_nuit', 'Temp Eco Nuit', 18);
new_payload.TEMP_CONFORT_ETE = getNumericState('input_number.temp_confort_ete', 'Temp Confort Ete', 24);
new_payload.TEMP_HORS_GEL = getNumericState('input_number.temp_hors_gel', 'Temp Hors Gel', 16);
// Horaires
new_payload.ACTIVATION_PRECHAUFFAGE_MATIN = getState('input_boolean.activation_prechauffage_matin', 'Activation Préchauffage Matin', 'off') === 'on';
new_payload.HEURE_DEBUT_PRECHAUFFAGE_MATIN = getNumericState('input_number.heure_debut_prechauffage_matin', 'Heure Début Préchauffage Matin', 5);
new_payload.HEURE_FIN_PRECHAUFFAGE_MATIN = getNumericState('input_number.heure_fin_prechauffage_matin', 'Heure Fin Préchauffage Matin', 7);
new_payload.ACTIVATION_PRECHAUFFAGE_SOIR = getState('input_boolean.activation_prechauffage_soir', 'Activation Préchauffage Soir', 'off') === 'on';
new_payload.HEURE_DEBUT_PRECHAUFFAGE_SOIR = getNumericState('input_number.heure_debut_prechauffage_soir', 'Heure Début Préchauffage Soir', 20);
new_payload.HEURE_FIN_PRECHAUFFAGE_SOIR = getNumericState('input_number.heure_fin_prechauffage_soir', 'Heure Fin Préchauffage Soir', 22);
new_payload.VENTILATION_AUTO_MI_SAISON = getState('input_boolean.ventilation_auto_mi_saison', 'Ventilation Mi-Saison', 'off') === 'on';
new_payload.HEURE_DEBUT_VENTILATION = getNumericState('input_number.heure_debut_ventilation_mi_saison', 'Heure Début Ventilation', 10);
new_payload.HEURE_FIN_VENTILATION = getNumericState('input_number.heure_fin_ventilation_mi_saison', 'Heure Fin Ventilation', 14);
// Voiture
new_payload.SEUIL_SURPLUS_SOLAIRE_VOITURE = getNumericState('input_number.seuil_surplus_solaire_voiture', 'Seuil Surplus Voiture', 1000);
new_payload.SEUIL_BATTERIE_VOITURE_MAX = getNumericState('input_number.seuil_batterie_voiture_max', 'Seuil Max Voiture', 90);
// Gestion dynamique
new_payload.AJUSTEMENT_TEMP_DYNAMIQUE = getNumericState('input_number.ajustement_temp_dynamique', 'Ajustement Temp Dynamique', 1.0);
new_payload.SEUIL_MODULATION_ECO = getNumericState('input_number.seuil_modulation_eco', 'Seuil Modulation Eco', -2000);
new_payload.SEUIL_BOOST_SOLAIRE = getNumericState('input_number.seuil_boost_solaire', 'Seuil Boost Solaire', 1500);
new_payload.SEUIL_FROID_EXTERIEUR = getNumericState('input_number.seuil_froid_exterieur', 'Seuil Froid Extérieur', 0);
new_payload.SEUIL_CHAUD_EXTERIEUR = getNumericState('input_number.seuil_chaud_exterieur', 'Seuil Chaud Extérieur', 35);
// Forçage Manuel et activation
new_payload.DUREE_FORCAGE_MANUEL = getNumericState('input_number.duree_forcage_manuel', 'Durée Forçage Manuel', 3);
new_payload.HEURE_RAPPORT_QUOTIDIEN = getNumericState('input_number.heure_rapport_quotidien', 'Heure Rapport Quotidien', 12);
new_payload.MINUTE_RAPPORT_QUOTIDIEN = getNumericState('input_number.minute_rapport_quotidien', 'Minute Rapport Quotidien', 30);
new_payload.GESTION_AUTO_BUREAU = getState('input_boolean.gestion_auto_bureau', 'Gestion Bureau', 'on') === 'on';
new_payload.GESTION_AUTO_SALON = getState('input_boolean.gestion_auto_salon', 'Gestion Salon', 'on') === 'on';
new_payload.GESTION_AUTO_CH_PARENTS = getState('input_boolean.gestion_auto_ch_parents', 'Gestion Ch Parents', 'on') === 'on';
new_payload.GESTION_AUTO_CH_LEA = getState('input_boolean.gestion_auto_ch_lea', 'Gestion Ch Léa', 'on') === 'on';
new_payload.GESTION_AUTO_CH_CELIA = getState('input_boolean.gestion_auto_ch_celia', 'Gestion Ch Célia', 'on') === 'on';


// --- 5. Remplissage du reste du payload ---
const tempo = getState('sensor.rte_tempo_couleur_actuelle', 'Couleur Tempo', 'Bleu');
new_payload.COULEUR_TEMPO = tempo.charAt(0).toUpperCase() + tempo.slice(1).toLowerCase();
new_payload.HEURE = now.getHours();
new_payload.MINUTE = now.getMinutes();
new_payload.NIVEAU_BATTERIE_VOITURE = getNumericState('sensor.volvo_xc40_batterie', 'Niveau Batterie Voiture', 0);

// --- CORRECTION DE LA LOGIQUE DE SURPLUS ---
// On récupère la valeur brute du réseau. Positive = import, Négative = export.
const puissanceReseau = getNumericState('sensor.sofar_grid_power', 'Puissance Réseau', 0);
// Le surplus n'existe que si on exporte (valeur négative). On le convertit en nombre positif.
new_payload.SURPLUS_PUISSANCE = (puissanceReseau < 0) ? -puissanceReseau : 0;

new_payload.NIVEAU_BATTERIE_MAISON = getNumericState('sensor.sofar_battery_soc', 'Niveau Batterie Maison', 0);
new_payload.RAPPORT_DEBUG_MODE = getState('input_boolean.rapports_maison', 'Mode Debug Rapport', 'off') === 'on';
new_payload.TEMP_EXTERIEURE_ACTUELLE = getNumericState('sensor.openweathermap_temperature', 'Température Extérieure', 15);


// --- 6. Finalisation ---
new_payload.SENSOR_ERRORS = SENSOR_ERRORS;
msg.payload = new_payload;

return msg;

CODE DU NŒUD : LOGIQUE PRINCIPALE (Fonction)

try {
    // --- DÉBUT DU SCRIPT ---
    const payload = msg.payload;

    // Décomposition du payload
    const {
        COULEUR_TEMPO, HEURE, MINUTE, MODE_SAISONNIER, MAISON_INHABITEE,
        METEO_JOUR, GEMINI_FAILED, RAPPORT_DEBUG_MODE, SENSOR_ERRORS = [],
        NIVEAU_BATTERIE_VOITURE = 0, SURPLUS_PUISSANCE = 0, NIVEAU_BATTERIE_MAISON = 0,
        CONSIGNES_ACTUELLES, ETATS_CLIM, DUREE_FORCAGE_MANUEL, TEMP_EXTERIEURE_ACTUELLE,
        TEMP_CONFORT_HIVER, TEMP_ECO_NUIT, TEMP_CONFORT_ETE, TEMP_HORS_GEL,
        ACTIVATION_PRECHAUFFAGE_MATIN, HEURE_DEBUT_PRECHAUFFAGE_MATIN, HEURE_FIN_PRECHAUFFAGE_MATIN,
        ACTIVATION_PRECHAUFFAGE_SOIR, HEURE_DEBUT_PRECHAUFFAGE_SOIR, HEURE_FIN_PRECHAUFFAGE_SOIR,
        SEUIL_SURPLUS_SOLAIRE_VOITURE, SEUIL_BATTERIE_VOITURE_MAX,
        GESTION_AUTO_BUREAU, GESTION_AUTO_SALON, GESTION_AUTO_CH_PARENTS,
        GESTION_AUTO_CH_LEA, GESTION_AUTO_CH_CELIA,
        VENTILATION_AUTO_MI_SAISON, HEURE_DEBUT_VENTILATION, HEURE_FIN_VENTILATION,
        AJUSTEMENT_TEMP_DYNAMIQUE, SEUIL_MODULATION_ECO, SEUIL_BOOST_SOLAIRE,
        SEUIL_FROID_EXTERIEUR, SEUIL_CHAUD_EXTERIEUR,
        HEURE_RAPPORT_QUOTIDIEN, MINUTE_RAPPORT_QUOTIDIEN
    } = payload;
    
    let commands = [];
    const PIECES = ['BUREAU', 'SALON', 'CHAMBRE_PARENTS', 'CHAMBRE_LEA', 'CHAMBRE_CELIA'];
    const CHAMBRES_IDS = ['CHAMBRE_PARENTS', 'CHAMBRE_LEA', 'CHAMBRE_CELIA'];
    const PIECES_VIE_IDS = ['BUREAU', 'SALON'];
    
    // Variables pour le "bulletin de décision" du rapport
    let rapportAjustementTemp = "Aucun";
    let rapportActionBatterie = "AUTO"; 

    // --- DÉBUT DE LA LOGIQUE DE DÉCISION ---

    function getTempConfortDynamique(baseTemp, saison) {
        let tempAjustee = baseTemp;
        const isDaytime = (HEURE >= 7 && HEURE < 21);
        
        if (isDaytime && SURPLUS_PUISSANCE > SEUIL_BOOST_SOLAIRE) {
            rapportAjustementTemp = "Boost Solaire";
            if (saison === 'HIVER') tempAjustee += AJUSTEMENT_TEMP_DYNAMIQUE;
            else if (saison === 'ÉTÉ') tempAjustee -= AJUSTEMENT_TEMP_DYNAMIQUE;
        } else if (SURPLUS_PUISSANCE < SEUIL_MODULATION_ECO) {
            rapportAjustementTemp = "Eco Modulation";
            if (saison === 'HIVER') tempAjustee -= AJUSTEMENT_TEMP_DYNAMIQUE;
            else if (saison === 'ÉTÉ') tempAjustee += AJUSTEMENT_TEMP_DYNAMIQUE;
        } else {
            if (saison === 'HIVER' && TEMP_EXTERIEURE_ACTUELLE < SEUIL_FROID_EXTERIEUR) {
                rapportAjustementTemp = "Froid Extérieur";
                tempAjustee += AJUSTEMENT_TEMP_DYNAMIQUE;
            } else if (saison === 'ÉTÉ' && TEMP_EXTERIEURE_ACTUELLE > SEUIL_CHAUD_EXTERIEUR) {
                rapportAjustementTemp = "Chaleur Extérieure";
                tempAjustee -= AJUSTEMENT_TEMP_DYNAMIQUE;
            }
        }
        return tempAjustee;
    }

    function setClim(piece, state, temp = null) {
        const activationMap = {
            'BUREAU': GESTION_AUTO_BUREAU, 'SALON': GESTION_AUTO_SALON,
            'CHAMBRE_PARENTS': GESTION_AUTO_CH_PARENTS,
            'CHAMBRE_LEA': GESTION_AUTO_CH_LEA, 'CHAMBRE_CELIA': GESTION_AUTO_CH_CELIA
        };
        if (!activationMap[piece]) { return; }

        const tempIdeale = flow.get(`last_temp_set_${piece}`) || temp;
        const etatActuel = ETATS_CLIM[piece];
        const overrideTimestamp = flow.get(`override_timestamp_${piece}`);
        const manualOverrideActive = (etatActuel !== 'off' && temp !== null && CONSIGNES_ACTUELLES[piece] !== tempIdeale);

        if (manualOverrideActive) {
            const now = Date.now();
            if (!overrideTimestamp) { flow.set(`override_timestamp_${piece}`, now); }
            const elapsedHours = overrideTimestamp ? (now - overrideTimestamp) / 3600000 : 0;
            if (elapsedHours < DUREE_FORCAGE_MANUEL) { return; }
            flow.set(`override_timestamp_${piece}`, null);
        } else {
            if(overrideTimestamp) flow.set(`override_timestamp_${piece}`, null);
        }

        if (state === 'ON') {
            commands.push(`THERMOSTAT_${piece}_ON`);
            if (temp !== null) {
                commands.push(`${piece}_TEMP_${temp}`);
                flow.set(`last_temp_set_${piece}`, temp);
            }
        } else if (state === 'OFF') {
            commands.push(`THERMOSTAT_${piece}_OFF`);
            if (temp !== null) {
                commands.push(`${piece}_TEMP_${temp}`);
                flow.set(`last_temp_set_${piece}`, temp);
            }
        }
    }

    // MODIFIÉ : Logique de charge voiture mise à jour
    function gestionVoiture() {
        const currentCarBattery = isNaN(NIVEAU_BATTERIE_VOITURE) ? 0 : NIVEAU_BATTERIE_VOITURE;
        const isOffPeakHours = HEURE >= 22 || HEURE < 6;

        // Si la batterie est pleine, on arrête toujours la charge
        if (currentCarBattery >= SEUIL_BATTERIE_VOITURE_MAX) {
            commands.push('CHARGE_VOITURE_OFF');
            return;
        }

        // Logique par couleur de jour
        switch (COULEUR_TEMPO) {
            case 'Rouge':
                // En jour rouge, charge uniquement en heures creuses.
                if (isOffPeakHours) { commands.push('CHARGE_VOITURE_ON'); } 
                else { commands.push('CHARGE_VOITURE_OFF'); }
                break;

            case 'Bleu':
                // En jour bleu, charge uniquement en heures creuses, pas en surplus solaire.
                if (isOffPeakHours) { commands.push('CHARGE_VOITURE_ON'); } 
                else { commands.push('CHARGE_VOITURE_OFF'); }
                break;
            
            case 'Blanc':
            default:
                // En jour blanc, charge en heures creuses OU avec le surplus solaire.
                if (isOffPeakHours || SURPLUS_PUISSANCE > SEUIL_SURPLUS_SOLAIRE_VOITURE) {
                    commands.push('CHARGE_VOITURE_ON');
                } else {
                    commands.push('CHARGE_VOITURE_OFF');
                }
                break;
        }
    }

    function gestionCommune() {
        if (MODE_SAISONNIER === 'ÉTÉ' || MODE_SAISONNIER === 'MI-SAISON-ETE') { commands.push('MODE_AUTO_PISCINE_ON'); }
        else { commands.push('MODE_AUTO_PISCINE_OFF'); }

        const isDaytimeRedRestriction = (COULEUR_TEMPO === 'Rouge' && HEURE >= 6 && HEURE < 22);

        if (MAISON_INHABITEE || isDaytimeRedRestriction) {
            PIECES.forEach(piece => setClim(piece, 'OFF', TEMP_HORS_GEL));
            commands.push('VMC_MODE_VACANCES');
        } else {
            commands.push('VMC_MODE_NORMAL');
            
            // MODIFIÉ : Ajout d'un ajustement de température pour les jours bleus
            let tempAjustementJourBleu = 0;
            if (COULEUR_TEMPO === 'Bleu') {
                if (MODE_SAISONNIER === 'HIVER') { tempAjustementJourBleu = -1; }
                else if (MODE_SAISONNIER === 'ÉTÉ') { tempAjustementJourBleu = 1; }
            }

            if (MODE_SAISONNIER === 'HIVER') {
                const tempConfortHiverAjustee = getTempConfortDynamique(TEMP_CONFORT_HIVER, 'HIVER') + tempAjustementJourBleu;
                PIECES.forEach(piece => commands.push(`${piece}_MODE_CHAUFFAGE`));

                let isPrechauffageMatin = false;
                let isPrechauffageSoir = false;

                if (COULEUR_TEMPO === 'Rouge') {
                    if (HEURE >= 5 && HEURE < 6) isPrechauffageMatin = true;
                    if (HEURE >= 22 && HEURE < 23) isPrechauffageSoir = true;
                } else {
                    if (ACTIVATION_PRECHAUFFAGE_MATIN && HEURE >= HEURE_DEBUT_PRECHAUFFAGE_MATIN && HEURE < HEURE_FIN_PRECHAUFFAGE_MATIN) { isPrechauffageMatin = true; }
                    if (ACTIVATION_PRECHAUFFAGE_SOIR && HEURE >= HEURE_DEBUT_PRECHAUFFAGE_SOIR && HEURE < HEURE_FIN_PRECHAUFFAGE_SOIR) { isPrechauffageSoir = true; }
                }
                
                const isDeepNight = (HEURE >= 23 || HEURE < 5);
                
                if (isDeepNight) {
                    CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', TEMP_ECO_NUIT));
                    PIECES_VIE_IDS.forEach(piece => setClim(piece, 'OFF', TEMP_HORS_GEL));
                } else {
                    PIECES_VIE_IDS.forEach(piece => setClim(piece, 'ON', tempConfortHiverAjustee));
                    if (isPrechauffageMatin || isPrechauffageSoir) {
                        CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', tempConfortHiverAjustee));
                    } else {
                        CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', TEMP_ECO_NUIT));
                    }
                }
            } else if (MODE_SAISONNIER === 'ÉTÉ') {
                const tempConfortEteAjustee = getTempConfortDynamique(TEMP_CONFORT_ETE, 'ÉTÉ') + tempAjustementJourBleu;
                PIECES.forEach(piece => {
                    commands.push(`${piece}_MODE_CLIMATISATION`);
                    setClim(piece, 'ON', tempConfortEteAjustee);
                });
            } else { // --- MI-SAISON ---
                const isMidSeason = (MODE_SAISONNIER === 'MI-SAISON-ETE' || MODE_SAISONNIER === 'MI-SAISON-HIVER');
                const isVentilationTime = (HEURE >= HEURE_DEBUT_VENTILATION && HEURE < HEURE_FIN_VENTILATION);
                
                if (VENTILATION_AUTO_MI_SAISON && isMidSeason && isVentilationTime) {
                    PIECES.forEach(piece => {
                        commands.push(`${piece}_MODE_VENTILATION`);
                        commands.push(`THERMOSTAT_${piece}_ON`);
                    });
                } else {
                    PIECES.forEach(piece => setClim(piece, 'OFF', TEMP_HORS_GEL));
                }
            }
        }
    }
    
    // MODIFIÉ : Logique de batterie mise à jour
    function gestionBatterie() {
        const isNightTime = HEURE >= 22 || HEURE < 6;

        switch (COULEUR_TEMPO) {
            case 'Bleu':
                // En jour bleu, on charge toujours la nuit.
                if (isNightTime) {
                    commands.push('PACK_BATTERIE_CHARGE');
                    rapportActionBatterie = "CHARGE";
                } else if (HEURE === 6) { // On repasse en auto à 6h
                    commands.push('PACK_BATTERIE_AUTO');
                    rapportActionBatterie = "AUTO";
                } else { // Le reste du temps
                    commands.push('PACK_BATTERIE_AUTO');
                    rapportActionBatterie = "AUTO";
                }
                break;
            case 'Blanc':
                // En jour blanc, on charge la nuit uniquement si la météo est mauvaise.
                if (isNightTime && METEO_JOUR === 'MAUVAISE') {
                    commands.push('PACK_BATTERIE_CHARGE');
                    rapportActionBatterie = "CHARGE (Météo)";
                } else if (HEURE === 6) { // On repasse en auto à 6h
                    commands.push('PACK_BATTERIE_AUTO');
                    rapportActionBatterie = "AUTO";
                } else { // Le reste du temps
                    commands.push('PACK_BATTERIE_AUTO');
                    rapportActionBatterie = "AUTO";
                }
                break;
            case 'Rouge':
                const isNightChargeTimeRed = (HEURE === 0 && MINUTE >= 30) || (HEURE >= 1 && HEURE < 6);
                if (isNightChargeTimeRed) {
                    commands.push('PACK_BATTERIE_CHARGE');
                    rapportActionBatterie = "CHARGE";
                } else if (HEURE === 6) {
                    commands.push('PACK_BATTERIE_AUTO'); // Passage en auto pour décharger sur la maison
                    rapportActionBatterie = "AUTO";
                } else if (HEURE > 6 && HEURE < 22) {
                    rapportActionBatterie = "DECHARGE"; // Action implicite du mode auto
                } else { // Après 22h, avant la recharge nocturne
                    commands.push('PACK_BATTERIE_STANDBY'); // On préserve le peu qu'il reste
                    rapportActionBatterie = "STANDBY";
                }
                break;
            default:
                commands.push('PACK_BATTERIE_AUTO');
                rapportActionBatterie = "AUTO";
                break;
        }
    }
    
    // --- EXÉCUTION DE LA LOGIQUE ---

    if (COULEUR_TEMPO === 'Rouge') {
        if (HEURE === 6) { commands.push('ECONOMIE_ECONOMIE_ON'); }
        if (HEURE === 22) { commands.push('ECONOMIE_ECONOMIE_OFF'); }
    }

    gestionVoiture();
    gestionCommune();
    gestionBatterie();
    
    if (GEMINI_FAILED) {
        commands.push('NOTIFIER_ERREUR_METEO');
    }

    // --- PRÉPARATION DES MESSAGES DE SORTIE ---
    const finalCommands = [...new Set(commands)];
    
    msg.payload.RAPPORT_AJUSTEMENT_TEMP = rapportAjustementTemp;
    msg.payload.RAPPORT_ACTION_BATTERIE = rapportActionBatterie;
    
    let msg_commandes = { payload: msg.payload, commands: finalCommands };
    let msg_rapport = null;

    const today = new Date().toISOString().slice(0, 10);
    const lastReportDate = flow.get('last_report_date') || '';
    const reportAlreadySentToday = (today === lastReportDate);
    const isTimeForDailyReport = (HEURE > HEURE_RAPPORT_QUOTIDIEN || (HEURE === HEURE_RAPPORT_QUOTIDIEN && MINUTE >= MINUTE_RAPPORT_QUOTIDIEN));

    if (RAPPORT_DEBUG_MODE || (!reportAlreadySentToday && isTimeForDailyReport)) {
        msg_rapport = { payload: msg.payload, commands: finalCommands };
        if (!RAPPORT_DEBUG_MODE) {
            flow.set('last_report_date', today);
        }
    }

    return [msg_commandes, msg_rapport];
} catch (e) {
    node.error(e.stack, msg);
    return null;
}

CODE DU NŒUD : GÉNÉRATION DU PROMPT GEMINI (Fonction)

const payload = msg.payload;
const commands = msg.commands || [];

const prompt = `
Rédige un rapport quotidien concis et agréable à lire pour la gestion de la maison, en te basant **uniquement** sur les données suivantes.

**Données Brutes :**
- Mode saisonnier : ${payload.MODE_SAISONNIER}
- Météo du jour : ${payload.METEO_JOUR}
- Tarif Tempo : ${payload.COULEUR_TEMPO}
- Commandes envoyées : ${commands.join(', ')}
- Raison de l'ajustement de température : "${payload.RAPPORT_AJUSTEMENT_TEMP}"
- Action décidée pour la batterie : "${payload.RAPPORT_ACTION_BATTERIE}"

**Instructions de Formatage :**
Structure ta réponse avec les sections suivantes :

*   **Chauffage / Clim** : Décris les actions sur les thermostats. **Pour une meilleure lisibilité, regroupe les actions pour les chambres (Parents, Léa, Célia) et pour les pièces de vie (Bureau, Salon).** Si la raison de l'ajustement de température n'est pas "Aucun", mentionne-la.
*   **Voiture Électrique** : Indique si la charge a été activée ou désactivée.
*   **Piscine** : Indique l'état du mode automatique.
*   **Batterie Maison** : Indique l'action exacte décidée (${payload.RAPPORT_ACTION_BATTERIE}).

**Touche Finale :**
Termine **toujours** le rapport par une citation inspirante ou une blague sur le thème de la domotique, la technologie ou la maison.
`;

msg.payload = prompt;

return msg;

CODE DU NOEUD REPARTITEUR:

// Ce noeud reçoit une commande climate (temp, on/off, ou mode)
const command = msg.payload;

// --- Correspondances ---
const entityMap = {
    'BUREAU': 'climate.bureau',
    'SALON': 'climate.salon',
    'CHAMBRE_PARENTS': 'climate.parents',
    'CHAMBRE_LEA': 'climate.lea',
    'CHAMBRE_CELIA': 'climate.celia'
};

// Traduction des modes pour Home Assistant
const modeMap = {
    'CHAUFFAGE': 'heat',
    'CLIMATISATION': 'cool',
    'VENTILATION': 'fan_only'
};

// --- Test 1: Commande de Température ---
// CORRIGÉ: Ajout de _TEMP_ pour capturer uniquement le nom de la pièce
let tempRegex = /^(.+)_TEMP_(\d+\.?\d*)$/; 
let match = command.match(tempRegex);

if (match) {
    const piece = match[1];
    const temperature = parseFloat(match[2]);
    const entityId = entityMap[piece];
    if (entityId) {
        msg.payload = {
            type: "call_service", domain: "climate", service: "set_temperature",
            service_data: { temperature: temperature },
            target: { entity_id: entityId }
        };
        return msg;
    }
}

// --- Test 2: Commande ON/OFF ---
// CORRIGÉ: Ajout de THERMOSTAT_ et _ pour capturer uniquement le nom de la pièce
let onOffRegex = /^THERMOSTAT_(.+)_(ON|OFF)$/;
match = command.match(onOffRegex);

if (match) {
    const piece = match[1];
    const state = match[2];
    const entityId = entityMap[piece];
    const service = (state === 'ON') ? 'turn_on' : 'turn_off';
    if (entityId) {
        msg.payload = {
            type: "call_service", domain: "climate", service: service,
            target: { entity_id: entityId }
        };
        return msg;
    }
}

// --- Test 3: Commande de Mode (Chauffage/Clim/Ventil) ---
// CORRIGÉ: Ajout de _MODE_ pour capturer uniquement le nom de la pièce
let modeRegex = /^(.+)_MODE_(CHAUFFAGE|CLIMATISATION|VENTILATION)$/;
match = command.match(modeRegex);

if (match) {
    const piece = match[1];
    const mode = match[2];
    const entityId = entityMap[piece];
    const hvacMode = modeMap[mode]; // Traduit 'CHAUFFAGE' en 'heat', etc.
    if (entityId && hvacMode) {
        msg.payload = {
            type: "call_service", domain: "climate", service: "set_hvac_mode",
            service_data: { hvac_mode: hvacMode },
            target: { entity_id: entityId }
        };
        return msg;
    }
}

// Si la commande ne correspond à aucun test, on l'ignore.
return null;

FLUX NODERED:

[
    {
        "id": "236bdcb1284426a5",
        "type": "tab",
        "label": "GEMINI GESTION MAISON",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "1934884e6acaaf6c",
        "type": "group",
        "z": "236bdcb1284426a5",
        "style": {
            "stroke": "#999999",
            "stroke-opacity": "1",
            "fill": "none",
            "fill-opacity": "1",
            "label": true,
            "label-position": "nw",
            "color": "#a4a4a4"
        },
        "nodes": [
            "381d7a4acb41d4f1",
            "0440734ffb117e0b",
            "3888edd9a1921ee2",
            "6ef2e69c91c99492",
            "9a482a2eba7531d1",
            "1731f716abe1bf2d",
            "792c83c59b62bdfc",
            "701e199ff346c6ac"
        ],
        "x": 2754,
        "y": 299,
        "w": 552,
        "h": 242
    },
    {
        "id": "30d526b71c5a89fc",
        "type": "group",
        "z": "236bdcb1284426a5",
        "style": {
            "stroke": "#999999",
            "stroke-opacity": "1",
            "fill": "none",
            "fill-opacity": "1",
            "label": true,
            "label-position": "nw",
            "color": "#a4a4a4"
        },
        "nodes": [
            "0462f0690349fd6d",
            "9dbfa8bafbdc3938"
        ],
        "x": 2754,
        "y": 559,
        "w": 312,
        "h": 142
    },
    {
        "id": "9f231d2df151524f",
        "type": "group",
        "z": "236bdcb1284426a5",
        "style": {
            "stroke": "#999999",
            "stroke-opacity": "1",
            "fill": "none",
            "fill-opacity": "1",
            "label": true,
            "label-position": "nw",
            "color": "#a4a4a4"
        },
        "nodes": [
            "b6e8a65a855cc5ab",
            "cdf300da4292e6e7",
            "7b1641a88e3b768a"
        ],
        "x": 2754,
        "y": 719,
        "w": 272,
        "h": 202
    },
    {
        "id": "e584f80d1f57904e",
        "type": "group",
        "z": "236bdcb1284426a5",
        "style": {
            "stroke": "#999999",
            "stroke-opacity": "1",
            "fill": "none",
            "fill-opacity": "1",
            "label": true,
            "label-position": "nw",
            "color": "#a4a4a4"
        },
        "nodes": [
            "b075d9b0eea63d81",
            "f5061a132134d437"
        ],
        "x": 2754,
        "y": 939,
        "w": 212,
        "h": 142
    },
    {
        "id": "2f5e2fcfbe552773",
        "type": "group",
        "z": "236bdcb1284426a5",
        "style": {
            "stroke": "#999999",
            "stroke-opacity": "1",
            "fill": "none",
            "fill-opacity": "1",
            "label": true,
            "label-position": "nw",
            "color": "#a4a4a4"
        },
        "nodes": [
            "4db9e0618b1396b1",
            "36388a7c171e859c",
            "0c2a1314b121cfe2",
            "f7056c1092e92d0b",
            "0600826375f8dd75",
            "f504f8535b3332cd",
            "06bb7e8c739fefe7",
            "bf050c8d7f1f56a9",
            "d4d45c71f888bbb8",
            "6d2550589dc092a8"
        ],
        "x": 2754,
        "y": 1099,
        "w": 332,
        "h": 442
    },
    {
        "id": "911ce4bfeaf3c135",
        "type": "group",
        "z": "236bdcb1284426a5",
        "style": {
            "stroke": "#999999",
            "stroke-opacity": "1",
            "fill": "none",
            "fill-opacity": "1",
            "label": true,
            "label-position": "nw",
            "color": "#a4a4a4"
        },
        "nodes": [
            "23193f1cf1ab4828",
            "3dc9dc2f1ee7e481",
            "567cfc1553529b90",
            "f83056ad321e8412",
            "f082998a84a2efd6",
            "18172beef3583059"
        ],
        "x": 2754,
        "y": 1559,
        "w": 632,
        "h": 222
    },
    {
        "id": "a9b8c7d6.e5f4g3",
        "type": "inject",
        "z": "236bdcb1284426a5",
        "name": "Bouton de Test",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 120,
        "y": 60,
        "wires": [
            [
                "e2c1a8d1.f8b3c8"
            ]
        ]
    },
    {
        "id": "e4a2a1b9.a5d3f",
        "type": "cronplus",
        "z": "236bdcb1284426a5",
        "name": "Déclencheur Horaire",
        "outputField": "payload",
        "timeZone": "Europe/Paris",
        "storeName": "default",
        "commandResponseMsgOutput": "output1",
        "defaultLocation": "",
        "defaultLocationType": "default",
        "outputs": 1,
        "options": [
            {
                "name": "schedule",
                "topic": "schedule",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "30 0 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule9",
                "topic": "schedule9",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "0 02 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule10",
                "topic": "schedule10",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "0 04 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule2",
                "topic": "schedule2",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "0 6 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule3",
                "topic": "schedule3",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "0 8 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule4",
                "topic": "schedule4",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "30 12 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule5",
                "topic": "schedule5",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "0 14 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule6",
                "topic": "schedule6",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "0 18 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule7",
                "topic": "schedule7",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "0 20 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule8",
                "topic": "schedule8",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "0 22 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            }
        ],
        "x": 130,
        "y": 120,
        "wires": [
            [
                "e2c1a8d1.f8b3c8",
                "40ada44d0756d5bc"
            ]
        ]
    },
    {
        "id": "e2c1a8d1.f8b3c8",
        "type": "function",
        "z": "236bdcb1284426a5",
        "name": "1. Préparer le Prompt Météo",
        "func": "msg.payload = `Tu es un assistant météo. Analyse les prévisions pour aujourd'hui dans la ville de Vergeze, France.\nTa réponse doit être **uniquement et exclusivement** l'objet JSON lui-même, sans aucun formatage markdown, sans backticks (\\`\\`\\`), et sans le mot 'json'.\n\nLa réponse doit être un objet JSON valide contenant deux clés :\n1.  \"temp_max\": la température maximale prévue (en tant que nombre).\n2.  \"meteo_jour\": la valeur \"BONNE\" si le temps est majoritairement ensoleillé, et \"MAUVAISE\" sinon.\n\nExemple de réponse attendue : {\"temp_max\": 12, \"meteo_jour\": \"MAUVAISE\"}`;\n\nreturn msg;",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 380,
        "y": 120,
        "wires": [
            [
                "d3a3f5f5.a1b4e8",
                "fc072eb2d12bf155",
                "9939794955042d17"
            ]
        ]
    },
    {
        "id": "d3a3f5f5.a1b4e8",
        "type": "trigger",
        "z": "236bdcb1284426a5",
        "name": "Timeout 2 minutes",
        "op1": "",
        "op2": "MAUVAISE",
        "op1type": "nul",
        "op2type": "str",
        "duration": "2",
        "extend": false,
        "overrideDelay": false,
        "units": "min",
        "reset": "reset",
        "bytopic": "all",
        "topic": "topic",
        "outputs": 1,
        "x": 670,
        "y": 220,
        "wires": [
            [
                "b2c1d0e1.fedcba"
            ]
        ]
    },
    {
        "id": "b2c1d0e1.fedcba",
        "type": "function",
        "z": "236bdcb1284426a5",
        "name": "Météo en échec",
        "func": "// --- NŒUD DE SECOURS SÉCURISÉ (V2) ---\n\nconst states = global.get('homeassistant.homeAssistant.states');\n\n// Fonctions sécurisées pour récupérer les états sans planter\nfunction getSafeState(entityId, defaultValue) {\n    const entity = states[entityId];\n    if (!entity || entity.state === 'unavailable' || entity.state === 'unknown') {\n        return defaultValue;\n    }\n    return entity.state;\n}\n\nfunction getSafeNumericState(entityId, defaultValue) {\n    const state = getSafeState(entityId, defaultValue.toString());\n    const value = parseFloat(state);\n    return isNaN(value) ? defaultValue : value;\n}\n\n// Récupération des données météo locales\nconst temperatureState = getSafeState('sensor.openweathermap_apparent_temperature', null);\nconst temperature = (temperatureState === null) ? null : parseFloat(temperatureState);\n\nconst cloudCoverage = getSafeNumericState('sensor.openweathermap_cloud_coverage', 100);\nconst condition = getSafeState('sensor.openweathermap_condition', 'cloudy').toLowerCase();\n\nlet meteoJour = \"MAUVAISE\";\n\n// Logique pour déterminer si la météo est \"BONNE\"\nconst goodConditions = ['sunny', 'partlycloudy', 'clear-night'];\nif (cloudCoverage < 50 && goodConditions.includes(condition)) {\n    meteoJour = \"BONNE\";\n}\n\n// On construit le payload. temp_max sera null si le capteur a échoué.\nmsg.payload = {\n    temp_max: isNaN(temperature) ? null : temperature,\n    meteo_jour: meteoJour\n};\n\n// On ajoute le flag pour signaler que le mode secours a été utilisé\nmsg.gemini_failed = true;\n\nreturn msg;",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1300,
        "y": 220,
        "wires": [
            [
                "c3f2d1e2.abcdef"
            ]
        ]
    },
    {
        "id": "c3f2d1e2.abcdef",
        "type": "join",
        "z": "236bdcb1284426a5",
        "name": "Attend Météo OU Timeout",
        "mode": "custom",
        "build": "merged",
        "property": "payload",
        "propertyType": "msg",
        "key": "topic",
        "joiner": "\\n",
        "joinerType": "str",
        "useparts": true,
        "accumulate": false,
        "timeout": "",
        "count": "1",
        "reduceRight": false,
        "reduceExp": "",
        "reduceInit": "",
        "reduceInitType": "",
        "reduceFixup": "",
        "x": 1340,
        "y": 120,
        "wires": [
            [
                "e91a1b1b.16e5e8",
                "5f9bc5b23a235478"
            ]
        ]
    },
    {
        "id": "e91a1b1b.16e5e8",
        "type": "function",
        "z": "236bdcb1284426a5",
        "name": "2. Définir les Variables de Contexte",
        "func": "// --- Début du script ---\nconst states = global.get('homeassistant.homeAssistant.states');\nconst now = new Date();\nconst currentMonth = now.getMonth() + 1; // +1 car les mois vont de 0 à 11\n\nlet new_payload = {};\nlet SENSOR_ERRORS = [];\n\n// --- Fonctions sécurisées ---\nfunction getState(entityId, sensorName, defaultValue) {\n    const entity = states[entityId];\n    if (!entity || entity.state === 'unavailable' || entity.state === 'unknown') {\n        SENSOR_ERRORS.push(sensorName);\n        return defaultValue;\n    }\n    return entity.state;\n}\n\nfunction getNumericState(entityId, sensorName, defaultValue) {\n    const state = getState(entityId, sensorName, defaultValue.toString());\n    const value = parseFloat(state);\n    if (isNaN(value)) {\n        if (!SENSOR_ERRORS.includes(sensorName)) { SENSOR_ERRORS.push(sensorName); }\n        return defaultValue;\n    }\n    return value;\n}\n\nfunction getNumericAttribute(entityId, attributeName, sensorName, defaultValue) {\n    const entity = states[entityId];\n    if (!entity || !entity.attributes) {\n        SENSOR_ERRORS.push(sensorName);\n        return defaultValue;\n    }\n    if (!entity.attributes.hasOwnProperty(attributeName) || entity.attributes[attributeName] === null) {\n        return defaultValue;\n    }\n    const value = parseFloat(entity.attributes[attributeName]);\n    if (isNaN(value)) {\n        if (!SENSOR_ERRORS.includes(sensorName)) { SENSOR_ERRORS.push(sensorName); }\n        return defaultValue;\n    }\n    return value;\n}\n\n// --- 1. LOGIQUE D'ANALYSE MÉTÉO SIMPLIFIÉE ---\nconst geminiResponse = msg.payload;\nnew_payload.GEMINI_FAILED = msg.gemini_failed || false;\nconst tempMax = geminiResponse.temp_max;\nnew_payload.METEO_JOUR = geminiResponse.meteo_jour || 'MAUVAISE';\n\n// --- LOGIQUE DE SAISON 100% FIABLE (MISE À JOUR) ---\nif (tempMax !== null) {\n    if (tempMax < 14) { new_payload.MODE_SAISONNIER = 'HIVER'; } \n    else if (tempMax > 23) { new_payload.MODE_SAISONNIER = 'ÉTÉ'; }\n    else {\n        if ([12, 1, 2].includes(currentMonth)) { new_payload.MODE_SAISONNIER = 'HIVER'; }\n        else if ([3, 4, 5, 6].includes(currentMonth)) { new_payload.MODE_SAISONNIER = 'MI-SAISON-ETE'; }\n        else if ([7, 8].includes(currentMonth)) { new_payload.MODE_SAISONNIER = 'ÉTÉ'; }\n        else { new_payload.MODE_SAISONNIER = 'MI-SAISON-HIVER'; }\n    }\n} else {\n    node.warn(\"Température max indisponible. Définition de la saison par le mois uniquement.\");\n    if ([12, 1, 2].includes(currentMonth)) { new_payload.MODE_SAISONNIER = 'HIVER'; }\n    else if ([6, 7, 8].includes(currentMonth)) { new_payload.MODE_SAISONNIER = 'ÉTÉ'; }\n    else if ([3, 4, 5].includes(currentMonth)) { new_payload.MODE_SAISONNIER = 'MI-SAISON-ETE'; }\n    else { new_payload.MODE_SAISONNIER = 'MI-SAISON-HIVER'; }\n}\n\n// --- 2. Logique de présence ---\nconst presenceState = getState('sensor.presence_sensor', 'Capteur de présence', 'Vide');\nconst alarmeState = getState('alarm_control_panel.alarmo', 'Alarme', 'disarmed');\nconst alarmeActivee = alarmeState !== 'disarmed';\nnew_payload.MAISON_INHABITEE = (presenceState === 'Vide') || alarmeActivee;\n\n// --- 3. Récupération des états et consignes des thermostats ---\nnew_payload.CONSIGNES_ACTUELLES = {\n    'BUREAU': getNumericAttribute('climate.bureau', 'temperature', 'Consigne Bureau', 0),\n    'SALON': getNumericAttribute('climate.salon', 'temperature', 'Consigne Salon', 0),\n    'CHAMBRE_PARENTS': getNumericAttribute('climate.parents', 'temperature', 'Consigne Chambre Parents', 0),\n    'CHAMBRE_LEA': getNumericAttribute('climate.lea', 'temperature', 'Consigne Chambre Léa', 0),\n    'CHAMBRE_CELIA': getNumericAttribute('climate.celia', 'temperature', 'Consigne Chambre Célia', 0)\n};\nnew_payload.ETATS_CLIM = {\n    'BUREAU': getState('climate.bureau', 'État Bureau', 'off'),\n    'SALON': getState('climate.salon', 'État Salon', 'off'),\n    'CHAMBRE_PARENTS': getState('climate.parents', 'État Chambre Parents', 'off'),\n    'CHAMBRE_LEA': getState('climate.lea', 'État Chambre Léa', 'off'),\n    'CHAMBRE_CELIA': getState('climate.celia', 'État Chambre Célia', 'off')\n};\n\n// --- 4. Récupération des Paramètres (Helpers) ---\n// Températures\nnew_payload.TEMP_CONFORT_HIVER = getNumericState('input_number.temp_confort_hiver', 'Temp Confort Hiver', 20);\nnew_payload.TEMP_ECO_NUIT = getNumericState('input_number.temp_eco_nuit', 'Temp Eco Nuit', 18);\nnew_payload.TEMP_CONFORT_ETE = getNumericState('input_number.temp_confort_ete', 'Temp Confort Ete', 24);\nnew_payload.TEMP_HORS_GEL = getNumericState('input_number.temp_hors_gel', 'Temp Hors Gel', 16);\n// Horaires\nnew_payload.ACTIVATION_PRECHAUFFAGE_MATIN = getState('input_boolean.activation_prechauffage_matin', 'Activation Préchauffage Matin', 'off') === 'on';\nnew_payload.HEURE_DEBUT_PRECHAUFFAGE_MATIN = getNumericState('input_number.heure_debut_prechauffage_matin', 'Heure Début Préchauffage Matin', 5);\nnew_payload.HEURE_FIN_PRECHAUFFAGE_MATIN = getNumericState('input_number.heure_fin_prechauffage_matin', 'Heure Fin Préchauffage Matin', 7);\nnew_payload.ACTIVATION_PRECHAUFFAGE_SOIR = getState('input_boolean.activation_prechauffage_soir', 'Activation Préchauffage Soir', 'off') === 'on';\nnew_payload.HEURE_DEBUT_PRECHAUFFAGE_SOIR = getNumericState('input_number.heure_debut_prechauffage_soir', 'Heure Début Préchauffage Soir', 20);\nnew_payload.HEURE_FIN_PRECHAUFFAGE_SOIR = getNumericState('input_number.heure_fin_prechauffage_soir', 'Heure Fin Préchauffage Soir', 22);\nnew_payload.VENTILATION_AUTO_MI_SAISON = getState('input_boolean.ventilation_auto_mi_saison', 'Ventilation Mi-Saison', 'off') === 'on';\nnew_payload.HEURE_DEBUT_VENTILATION = getNumericState('input_number.heure_debut_ventilation_mi_saison', 'Heure Début Ventilation', 10);\nnew_payload.HEURE_FIN_VENTILATION = getNumericState('input_number.heure_fin_ventilation_mi_saison', 'Heure Fin Ventilation', 14);\n// Voiture\nnew_payload.SEUIL_SURPLUS_SOLAIRE_VOITURE = getNumericState('input_number.seuil_surplus_solaire_voiture', 'Seuil Surplus Voiture', 1000);\nnew_payload.SEUIL_BATTERIE_VOITURE_MAX = getNumericState('input_number.seuil_batterie_voiture_max', 'Seuil Max Voiture', 90);\n// Gestion dynamique\nnew_payload.AJUSTEMENT_TEMP_DYNAMIQUE = getNumericState('input_number.ajustement_temp_dynamique', 'Ajustement Temp Dynamique', 1.0);\nnew_payload.SEUIL_MODULATION_ECO = getNumericState('input_number.seuil_modulation_eco', 'Seuil Modulation Eco', -2000);\nnew_payload.SEUIL_BOOST_SOLAIRE = getNumericState('input_number.seuil_boost_solaire', 'Seuil Boost Solaire', 1500);\nnew_payload.SEUIL_FROID_EXTERIEUR = getNumericState('input_number.seuil_froid_exterieur', 'Seuil Froid Extérieur', 0);\nnew_payload.SEUIL_CHAUD_EXTERIEUR = getNumericState('input_number.seuil_chaud_exterieur', 'Seuil Chaud Extérieur', 35);\n// Forçage Manuel et activation\nnew_payload.DUREE_FORCAGE_MANUEL = getNumericState('input_number.duree_forcage_manuel', 'Durée Forçage Manuel', 3);\nnew_payload.HEURE_RAPPORT_QUOTIDIEN = getNumericState('input_number.heure_rapport_quotidien', 'Heure Rapport Quotidien', 12);\nnew_payload.MINUTE_RAPPORT_QUOTIDIEN = getNumericState('input_number.minute_rapport_quotidien', 'Minute Rapport Quotidien', 30);\nnew_payload.GESTION_AUTO_BUREAU = getState('input_boolean.gestion_auto_bureau', 'Gestion Bureau', 'on') === 'on';\nnew_payload.GESTION_AUTO_SALON = getState('input_boolean.gestion_auto_salon', 'Gestion Salon', 'on') === 'on';\nnew_payload.GESTION_AUTO_CH_PARENTS = getState('input_boolean.gestion_auto_ch_parents', 'Gestion Ch Parents', 'on') === 'on';\nnew_payload.GESTION_AUTO_CH_LEA = getState('input_boolean.gestion_auto_ch_lea', 'Gestion Ch Léa', 'on') === 'on';\nnew_payload.GESTION_AUTO_CH_CELIA = getState('input_boolean.gestion_auto_ch_celia', 'Gestion Ch Célia', 'on') === 'on';\n\n\n// --- 5. Remplissage du reste du payload ---\nconst tempo = getState('sensor.rte_tempo_couleur_actuelle', 'Couleur Tempo', 'Bleu');\nnew_payload.COULEUR_TEMPO = tempo.charAt(0).toUpperCase() + tempo.slice(1).toLowerCase();\nnew_payload.HEURE = now.getHours();\nnew_payload.MINUTE = now.getMinutes();\nnew_payload.NIVEAU_BATTERIE_VOITURE = getNumericState('sensor.volvo_xc40_batterie', 'Niveau Batterie Voiture', 0);\n\n// --- CORRECTION DE LA LOGIQUE DE SURPLUS ---\n// On récupère la valeur brute du réseau. Positive = import, Négative = export.\nconst puissanceReseau = getNumericState('sensor.sofar_grid_power', 'Puissance Réseau', 0);\n// Le surplus n'existe que si on exporte (valeur négative). On le convertit en nombre positif.\nnew_payload.SURPLUS_PUISSANCE = (puissanceReseau < 0) ? -puissanceReseau : 0;\n\nnew_payload.NIVEAU_BATTERIE_MAISON = getNumericState('sensor.sofar_battery_soc', 'Niveau Batterie Maison', 0);\nnew_payload.RAPPORT_DEBUG_MODE = getState('input_boolean.rapports_maison', 'Mode Debug Rapport', 'off') === 'on';\nnew_payload.TEMP_EXTERIEURE_ACTUELLE = getNumericState('sensor.openweathermap_temperature', 'Température Extérieure', 15);\n\n\n// --- 6. Finalisation ---\nnew_payload.SENSOR_ERRORS = SENSOR_ERRORS;\nmsg.payload = new_payload;\n\nreturn msg;",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1660,
        "y": 120,
        "wires": [
            [
                "c1a4e5c3.c8d318",
                "f9e8d7c6.b5a4b3"
            ]
        ]
    },
    {
        "id": "f9e8d7c6.b5a4b3",
        "type": "debug",
        "z": "236bdcb1284426a5",
        "name": "Variables de Contexte (Test)",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 1740,
        "y": 60,
        "wires": []
    },
    {
        "id": "c1a4e5c3.c8d318",
        "type": "function",
        "z": "236bdcb1284426a5",
        "name": "3. Appliquer les Règles & Générer les Commandes",
        "func": "try {\n    // --- DÉBUT DU SCRIPT ---\n    const payload = msg.payload;\n\n    // Décomposition du payload\n    const {\n        COULEUR_TEMPO, HEURE, MINUTE, MODE_SAISONNIER, MAISON_INHABITEE,\n        METEO_JOUR, GEMINI_FAILED, RAPPORT_DEBUG_MODE, SENSOR_ERRORS = [],\n        NIVEAU_BATTERIE_VOITURE = 0, SURPLUS_PUISSANCE = 0, NIVEAU_BATTERIE_MAISON = 0,\n        CONSIGNES_ACTUELLES, ETATS_CLIM, DUREE_FORCAGE_MANUEL, TEMP_EXTERIEURE_ACTUELLE,\n        TEMP_CONFORT_HIVER, TEMP_ECO_NUIT, TEMP_CONFORT_ETE, TEMP_HORS_GEL,\n        ACTIVATION_PRECHAUFFAGE_MATIN, HEURE_DEBUT_PRECHAUFFAGE_MATIN, HEURE_FIN_PRECHAUFFAGE_MATIN,\n        ACTIVATION_PRECHAUFFAGE_SOIR, HEURE_DEBUT_PRECHAUFFAGE_SOIR, HEURE_FIN_PRECHAUFFAGE_SOIR,\n        SEUIL_SURPLUS_SOLAIRE_VOITURE, SEUIL_BATTERIE_VOITURE_MAX,\n        GESTION_AUTO_BUREAU, GESTION_AUTO_SALON, GESTION_AUTO_CH_PARENTS,\n        GESTION_AUTO_CH_LEA, GESTION_AUTO_CH_CELIA,\n        VENTILATION_AUTO_MI_SAISON, HEURE_DEBUT_VENTILATION, HEURE_FIN_VENTILATION,\n        AJUSTEMENT_TEMP_DYNAMIQUE, SEUIL_MODULATION_ECO, SEUIL_BOOST_SOLAIRE,\n        SEUIL_FROID_EXTERIEUR, SEUIL_CHAUD_EXTERIEUR,\n        HEURE_RAPPORT_QUOTIDIEN, MINUTE_RAPPORT_QUOTIDIEN\n    } = payload;\n    \n    let commands = [];\n    const PIECES = ['BUREAU', 'SALON', 'CHAMBRE_PARENTS', 'CHAMBRE_LEA', 'CHAMBRE_CELIA'];\n    const CHAMBRES_IDS = ['CHAMBRE_PARENTS', 'CHAMBRE_LEA', 'CHAMBRE_CELIA'];\n    const PIECES_VIE_IDS = ['BUREAU', 'SALON'];\n    \n    // Variables pour le \"bulletin de décision\" du rapport\n    let rapportAjustementTemp = \"Aucun\";\n    let rapportActionBatterie = \"AUTO\"; \n\n    // --- DÉBUT DE LA LOGIQUE DE DÉCISION ---\n\n    function getTempConfortDynamique(baseTemp, saison) {\n        let tempAjustee = baseTemp;\n        const isDaytime = (HEURE >= 7 && HEURE < 21);\n        \n        if (isDaytime && SURPLUS_PUISSANCE > SEUIL_BOOST_SOLAIRE) {\n            rapportAjustementTemp = \"Boost Solaire\";\n            if (saison === 'HIVER') tempAjustee += AJUSTEMENT_TEMP_DYNAMIQUE;\n            else if (saison === 'ÉTÉ') tempAjustee -= AJUSTEMENT_TEMP_DYNAMIQUE;\n        } else if (SURPLUS_PUISSANCE < SEUIL_MODULATION_ECO) {\n            rapportAjustementTemp = \"Eco Modulation\";\n            if (saison === 'HIVER') tempAjustee -= AJUSTEMENT_TEMP_DYNAMIQUE;\n            else if (saison === 'ÉTÉ') tempAjustee += AJUSTEMENT_TEMP_DYNAMIQUE;\n        } else {\n            if (saison === 'HIVER' && TEMP_EXTERIEURE_ACTUELLE < SEUIL_FROID_EXTERIEUR) {\n                rapportAjustementTemp = \"Froid Extérieur\";\n                tempAjustee += AJUSTEMENT_TEMP_DYNAMIQUE;\n            } else if (saison === 'ÉTÉ' && TEMP_EXTERIEURE_ACTUELLE > SEUIL_CHAUD_EXTERIEUR) {\n                rapportAjustementTemp = \"Chaleur Extérieure\";\n                tempAjustee -= AJUSTEMENT_TEMP_DYNAMIQUE;\n            }\n        }\n        return tempAjustee;\n    }\n\n    function setClim(piece, state, temp = null) {\n        const activationMap = {\n            'BUREAU': GESTION_AUTO_BUREAU, 'SALON': GESTION_AUTO_SALON,\n            'CHAMBRE_PARENTS': GESTION_AUTO_CH_PARENTS,\n            'CHAMBRE_LEA': GESTION_AUTO_CH_LEA, 'CHAMBRE_CELIA': GESTION_AUTO_CH_CELIA\n        };\n        if (!activationMap[piece]) { return; }\n\n        const tempIdeale = flow.get(`last_temp_set_${piece}`) || temp;\n        const etatActuel = ETATS_CLIM[piece];\n        const overrideTimestamp = flow.get(`override_timestamp_${piece}`);\n        const manualOverrideActive = (etatActuel !== 'off' && temp !== null && CONSIGNES_ACTUELLES[piece] !== tempIdeale);\n\n        if (manualOverrideActive) {\n            const now = Date.now();\n            if (!overrideTimestamp) { flow.set(`override_timestamp_${piece}`, now); }\n            const elapsedHours = overrideTimestamp ? (now - overrideTimestamp) / 3600000 : 0;\n            if (elapsedHours < DUREE_FORCAGE_MANUEL) { return; }\n            flow.set(`override_timestamp_${piece}`, null);\n        } else {\n            if(overrideTimestamp) flow.set(`override_timestamp_${piece}`, null);\n        }\n\n        if (state === 'ON') {\n            commands.push(`THERMOSTAT_${piece}_ON`);\n            if (temp !== null) {\n                commands.push(`${piece}_TEMP_${temp}`);\n                flow.set(`last_temp_set_${piece}`, temp);\n            }\n        } else if (state === 'OFF') {\n            commands.push(`THERMOSTAT_${piece}_OFF`);\n            if (temp !== null) {\n                commands.push(`${piece}_TEMP_${temp}`);\n                flow.set(`last_temp_set_${piece}`, temp);\n            }\n        }\n    }\n\n    // MODIFIÉ : Logique de charge voiture mise à jour\n    function gestionVoiture() {\n        const currentCarBattery = isNaN(NIVEAU_BATTERIE_VOITURE) ? 0 : NIVEAU_BATTERIE_VOITURE;\n        const isOffPeakHours = HEURE >= 22 || HEURE < 6;\n\n        // Si la batterie est pleine, on arrête toujours la charge\n        if (currentCarBattery >= SEUIL_BATTERIE_VOITURE_MAX) {\n            commands.push('CHARGE_VOITURE_OFF');\n            return;\n        }\n\n        // Logique par couleur de jour\n        switch (COULEUR_TEMPO) {\n            case 'Rouge':\n                // En jour rouge, charge uniquement en heures creuses.\n                if (isOffPeakHours) { commands.push('CHARGE_VOITURE_ON'); } \n                else { commands.push('CHARGE_VOITURE_OFF'); }\n                break;\n\n            case 'Bleu':\n                // En jour bleu, charge uniquement en heures creuses, pas en surplus solaire.\n                if (isOffPeakHours) { commands.push('CHARGE_VOITURE_ON'); } \n                else { commands.push('CHARGE_VOITURE_OFF'); }\n                break;\n            \n            case 'Blanc':\n            default:\n                // En jour blanc, charge en heures creuses OU avec le surplus solaire.\n                if (isOffPeakHours || SURPLUS_PUISSANCE > SEUIL_SURPLUS_SOLAIRE_VOITURE) {\n                    commands.push('CHARGE_VOITURE_ON');\n                } else {\n                    commands.push('CHARGE_VOITURE_OFF');\n                }\n                break;\n        }\n    }\n\n    function gestionCommune() {\n        if (MODE_SAISONNIER === 'ÉTÉ' || MODE_SAISONNIER === 'MI-SAISON-ETE') { commands.push('MODE_AUTO_PISCINE_ON'); }\n        else { commands.push('MODE_AUTO_PISCINE_OFF'); }\n\n        const isDaytimeRedRestriction = (COULEUR_TEMPO === 'Rouge' && HEURE >= 6 && HEURE < 22);\n\n        if (MAISON_INHABITEE || isDaytimeRedRestriction) {\n            PIECES.forEach(piece => setClim(piece, 'OFF', TEMP_HORS_GEL));\n            commands.push('VMC_MODE_VACANCES');\n        } else {\n            commands.push('VMC_MODE_NORMAL');\n            \n            // MODIFIÉ : Ajout d'un ajustement de température pour les jours bleus\n            let tempAjustementJourBleu = 0;\n            if (COULEUR_TEMPO === 'Bleu') {\n                if (MODE_SAISONNIER === 'HIVER') { tempAjustementJourBleu = -1; }\n                else if (MODE_SAISONNIER === 'ÉTÉ') { tempAjustementJourBleu = 1; }\n            }\n\n            if (MODE_SAISONNIER === 'HIVER') {\n                const tempConfortHiverAjustee = getTempConfortDynamique(TEMP_CONFORT_HIVER, 'HIVER') + tempAjustementJourBleu;\n                PIECES.forEach(piece => commands.push(`${piece}_MODE_CHAUFFAGE`));\n\n                let isPrechauffageMatin = false;\n                let isPrechauffageSoir = false;\n\n                if (COULEUR_TEMPO === 'Rouge') {\n                    if (HEURE >= 5 && HEURE < 6) isPrechauffageMatin = true;\n                    if (HEURE >= 22 && HEURE < 23) isPrechauffageSoir = true;\n                } else {\n                    if (ACTIVATION_PRECHAUFFAGE_MATIN && HEURE >= HEURE_DEBUT_PRECHAUFFAGE_MATIN && HEURE < HEURE_FIN_PRECHAUFFAGE_MATIN) { isPrechauffageMatin = true; }\n                    if (ACTIVATION_PRECHAUFFAGE_SOIR && HEURE >= HEURE_DEBUT_PRECHAUFFAGE_SOIR && HEURE < HEURE_FIN_PRECHAUFFAGE_SOIR) { isPrechauffageSoir = true; }\n                }\n                \n                const isDeepNight = (HEURE >= 23 || HEURE < 5);\n                \n                if (isDeepNight) {\n                    CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', TEMP_ECO_NUIT));\n                    PIECES_VIE_IDS.forEach(piece => setClim(piece, 'OFF', TEMP_HORS_GEL));\n                } else {\n                    PIECES_VIE_IDS.forEach(piece => setClim(piece, 'ON', tempConfortHiverAjustee));\n                    if (isPrechauffageMatin || isPrechauffageSoir) {\n                        CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', tempConfortHiverAjustee));\n                    } else {\n                        CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', TEMP_ECO_NUIT));\n                    }\n                }\n            } else if (MODE_SAISONNIER === 'ÉTÉ') {\n                const tempConfortEteAjustee = getTempConfortDynamique(TEMP_CONFORT_ETE, 'ÉTÉ') + tempAjustementJourBleu;\n                PIECES.forEach(piece => {\n                    commands.push(`${piece}_MODE_CLIMATISATION`);\n                    setClim(piece, 'ON', tempConfortEteAjustee);\n                });\n            } else { // --- MI-SAISON ---\n                const isMidSeason = (MODE_SAISONNIER === 'MI-SAISON-ETE' || MODE_SAISONNIER === 'MI-SAISON-HIVER');\n                const isVentilationTime = (HEURE >= HEURE_DEBUT_VENTILATION && HEURE < HEURE_FIN_VENTILATION);\n                \n                if (VENTILATION_AUTO_MI_SAISON && isMidSeason && isVentilationTime) {\n                    PIECES.forEach(piece => {\n                        commands.push(`${piece}_MODE_VENTILATION`);\n                        commands.push(`THERMOSTAT_${piece}_ON`);\n                    });\n                } else {\n                    PIECES.forEach(piece => setClim(piece, 'OFF', TEMP_HORS_GEL));\n                }\n            }\n        }\n    }\n    \n    // MODIFIÉ : Logique de batterie mise à jour\n    function gestionBatterie() {\n        const isNightTime = HEURE >= 22 || HEURE < 6;\n\n        switch (COULEUR_TEMPO) {\n            case 'Bleu':\n                // En jour bleu, on charge toujours la nuit.\n                if (isNightTime) {\n                    commands.push('PACK_BATTERIE_CHARGE');\n                    rapportActionBatterie = \"CHARGE\";\n                } else if (HEURE === 6) { // On repasse en auto à 6h\n                    commands.push('PACK_BATTERIE_AUTO');\n                    rapportActionBatterie = \"AUTO\";\n                } else { // Le reste du temps\n                    commands.push('PACK_BATTERIE_AUTO');\n                    rapportActionBatterie = \"AUTO\";\n                }\n                break;\n            case 'Blanc':\n                // En jour blanc, on charge la nuit uniquement si la météo est mauvaise.\n                if (isNightTime && METEO_JOUR === 'MAUVAISE') {\n                    commands.push('PACK_BATTERIE_CHARGE');\n                    rapportActionBatterie = \"CHARGE (Météo)\";\n                } else if (HEURE === 6) { // On repasse en auto à 6h\n                    commands.push('PACK_BATTERIE_AUTO');\n                    rapportActionBatterie = \"AUTO\";\n                } else { // Le reste du temps\n                    commands.push('PACK_BATTERIE_AUTO');\n                    rapportActionBatterie = \"AUTO\";\n                }\n                break;\n            case 'Rouge':\n                const isNightChargeTimeRed = (HEURE === 0 && MINUTE >= 30) || (HEURE >= 1 && HEURE < 6);\n                if (isNightChargeTimeRed) {\n                    commands.push('PACK_BATTERIE_CHARGE');\n                    rapportActionBatterie = \"CHARGE\";\n                } else if (HEURE === 6) {\n                    commands.push('PACK_BATTERIE_AUTO'); // Passage en auto pour décharger sur la maison\n                    rapportActionBatterie = \"AUTO\";\n                } else if (HEURE > 6 && HEURE < 22) {\n                    rapportActionBatterie = \"DECHARGE\"; // Action implicite du mode auto\n                } else { // Après 22h, avant la recharge nocturne\n                    commands.push('PACK_BATTERIE_STANDBY'); // On préserve le peu qu'il reste\n                    rapportActionBatterie = \"STANDBY\";\n                }\n                break;\n            default:\n                commands.push('PACK_BATTERIE_AUTO');\n                rapportActionBatterie = \"AUTO\";\n                break;\n        }\n    }\n    \n    // --- EXÉCUTION DE LA LOGIQUE ---\n\n    if (COULEUR_TEMPO === 'Rouge') {\n        if (HEURE === 6) { commands.push('ECONOMIE_ECONOMIE_ON'); }\n        if (HEURE === 22) { commands.push('ECONOMIE_ECONOMIE_OFF'); }\n    }\n\n    gestionVoiture();\n    gestionCommune();\n    gestionBatterie();\n    \n    if (GEMINI_FAILED) {\n        commands.push('NOTIFIER_ERREUR_METEO');\n    }\n\n    // --- PRÉPARATION DES MESSAGES DE SORTIE ---\n    const finalCommands = [...new Set(commands)];\n    \n    msg.payload.RAPPORT_AJUSTEMENT_TEMP = rapportAjustementTemp;\n    msg.payload.RAPPORT_ACTION_BATTERIE = rapportActionBatterie;\n    \n    let msg_commandes = { payload: msg.payload, commands: finalCommands };\n    let msg_rapport = null;\n\n    const today = new Date().toISOString().slice(0, 10);\n    const lastReportDate = flow.get('last_report_date') || '';\n    const reportAlreadySentToday = (today === lastReportDate);\n    const isTimeForDailyReport = (HEURE > HEURE_RAPPORT_QUOTIDIEN || (HEURE === HEURE_RAPPORT_QUOTIDIEN && MINUTE >= MINUTE_RAPPORT_QUOTIDIEN));\n\n    if (RAPPORT_DEBUG_MODE || (!reportAlreadySentToday && isTimeForDailyReport)) {\n        msg_rapport = { payload: msg.payload, commands: finalCommands };\n        if (!RAPPORT_DEBUG_MODE) {\n            flow.set('last_report_date', today);\n        }\n    }\n\n    return [msg_commandes, msg_rapport];\n} catch (e) {\n    node.error(e.stack, msg);\n    return null;\n}",
        "outputs": 2,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 2050,
        "y": 120,
        "wires": [
            [
                "7e2dbd24a95ff56f",
                "94560f551005daa5"
            ],
            [
                "21e6c05cc60c61fe"
            ]
        ]
    },
    {
        "id": "e8e81d7f.c22d1",
        "type": "split",
        "z": "236bdcb1284426a5",
        "name": "",
        "splt": "",
        "spltType": "str",
        "arraySplt": "1",
        "arraySpltType": "len",
        "stream": false,
        "addname": "",
        "property": "payload",
        "x": 2670,
        "y": 120,
        "wires": [
            [
                "7560dd048b1773f8",
                "177b6a4071aa1684",
                "c0b22a0a.a2f648"
            ]
        ]
    },
    {
        "id": "c0b22a0a.a2f648",
        "type": "switch",
        "z": "236bdcb1284426a5",
        "name": "4. Aiguillage vers les Commandes",
        "property": "payload",
        "propertyType": "msg",
        "rules": [
            {
                "t": "eq",
                "v": "PACK_BATTERIE_AUTO",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "PACK_BATTERIE_CHARGE",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "PACK_BATTERIE_DISCHARGE",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "PACK_BATTERIE_STANDBY",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "MODE_AUTO_PISCINE_ON",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "MODE_AUTO_PISCINE_OFF",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "VMC_MODE_NORMAL",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "VMC_MODE_VACANCES",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "CHARGE_VOITURE_ON",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "CHARGE_VOITURE_OFF",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "ECONOMIE_ECONOMIE_ON",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "ECONOMIE_ECONOMIE_OFF",
                "vt": "str"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 12,
        "x": 2460,
        "y": 700,
        "wires": [
            [
                "6ef2e69c91c99492"
            ],
            [
                "0440734ffb117e0b"
            ],
            [
                "792c83c59b62bdfc"
            ],
            [
                "701e199ff346c6ac"
            ],
            [
                "0462f0690349fd6d"
            ],
            [
                "9dbfa8bafbdc3938"
            ],
            [
                "b6e8a65a855cc5ab"
            ],
            [
                "7b1641a88e3b768a"
            ],
            [
                "b075d9b0eea63d81"
            ],
            [
                "f5061a132134d437"
            ],
            [
                "4db9e0618b1396b1",
                "f7056c1092e92d0b",
                "0c2a1314b121cfe2",
                "0600826375f8dd75",
                "d4d45c71f888bbb8",
                "6d2550589dc092a8",
                "f504f8535b3332cd",
                "06bb7e8c739fefe7",
                "bf050c8d7f1f56a9"
            ],
            [
                "23193f1cf1ab4828",
                "3dc9dc2f1ee7e481",
                "f83056ad321e8412",
                "567cfc1553529b90"
            ]
        ]
    },
    {
        "id": "247c647c96be225d",
        "type": "gemini-generate-content",
        "z": "236bdcb1284426a5",
        "name": "",
        "apiKey": "fc6b0e5a17a8d62b",
        "modelSelection": "gemini-2.5-flash",
        "customModel": "",
        "customModelType": "str",
        "mode": "single",
        "prompt": "",
        "promptType": "str",
        "multimodalInputsData": "[]",
        "grounding": true,
        "temperature": "",
        "temperatureType": "num",
        "topP": "",
        "topPType": "num",
        "topK": "",
        "topKType": "num",
        "maxOutputTokens": "",
        "maxOutputTokensType": "num",
        "safetyHarassment": "",
        "safetyHateSpeech": "",
        "safetySexuallyExplicit": "",
        "safetyDangerousContent": "",
        "thinkingBudget": "",
        "thinkingBudgetType": "num",
        "includeThoughts": false,
        "systemInstruction": "",
        "systemInstructionType": "str",
        "outputProperty": "payload",
        "passthroughProperties": false,
        "responseFormat": "text",
        "schemaPropertiesData": "[]",
        "enumValues": "",
        "x": 1910,
        "y": 260,
        "wires": [
            [
                "c05179d91f2422d0",
                "8e0e828a4e6da63d"
            ],
            []
        ]
    },
    {
        "id": "9939794955042d17",
        "type": "gemini-generate-content",
        "z": "236bdcb1284426a5",
        "name": "",
        "apiKey": "fc6b0e5a17a8d62b",
        "modelSelection": "gemini-2.5-pro",
        "customModel": "",
        "customModelType": "str",
        "mode": "single",
        "prompt": "",
        "promptType": "str",
        "multimodalInputsData": "[]",
        "grounding": true,
        "temperature": "",
        "temperatureType": "num",
        "topP": "",
        "topPType": "num",
        "topK": "",
        "topKType": "num",
        "maxOutputTokens": "",
        "maxOutputTokensType": "num",
        "safetyHarassment": "",
        "safetyHateSpeech": "",
        "safetySexuallyExplicit": "",
        "safetyDangerousContent": "",
        "thinkingBudget": "",
        "thinkingBudgetType": "num",
        "includeThoughts": false,
        "systemInstruction": "",
        "systemInstructionType": "str",
        "outputProperty": "payload",
        "passthroughProperties": false,
        "responseFormat": "text",
        "schemaPropertiesData": "[]",
        "enumValues": "",
        "x": 670,
        "y": 120,
        "wires": [
            [
                "493a51d1d4a9e4ab",
                "395643a3832c70e9"
            ],
            []
        ]
    },
    {
        "id": "381d7a4acb41d4f1",
        "type": "mqtt out",
        "z": "236bdcb1284426a5",
        "g": "1934884e6acaaf6c",
        "name": "INVERTER_CHARGE_3000",
        "topic": "Sofar2mqtt/set/charge",
        "qos": "",
        "retain": "",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "3348075e0f83efab",
        "x": 3160,
        "y": 380,
        "wires": []
    },
    {
        "id": "0440734ffb117e0b",
        "type": "change",
        "z": "236bdcb1284426a5",
        "g": "1934884e6acaaf6c",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "3000",
                "tot": "str"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 2870,
        "y": 380,
        "wires": [
            [
                "381d7a4acb41d4f1"
            ]
        ]
    },
    {
        "id": "3888edd9a1921ee2",
        "type": "mqtt out",
        "z": "236bdcb1284426a5",
        "g": "1934884e6acaaf6c",
        "name": "INVERTER_AUTO",
        "topic": "Sofar2mqtt/set/auto",
        "qos": "2",
        "retain": "true",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "3348075e0f83efab",
        "x": 3130,
        "y": 340,
        "wires": []
    },
    {
        "id": "6ef2e69c91c99492",
        "type": "change",
        "z": "236bdcb1284426a5",
        "g": "1934884e6acaaf6c",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "true",
                "tot": "str"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 2870,
        "y": 340,
        "wires": [
            [
                "3888edd9a1921ee2"
            ]
        ]
    },
    {
        "id": "9a482a2eba7531d1",
        "type": "mqtt out",
        "z": "236bdcb1284426a5",
        "g": "1934884e6acaaf6c",
        "name": "INVERTER_DISCHARGE",
        "topic": "Sofar2mqtt/set/discharge",
        "qos": "",
        "retain": "",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "3348075e0f83efab",
        "x": 3150,
        "y": 440,
        "wires": []
    },
    {
        "id": "1731f716abe1bf2d",
        "type": "mqtt out",
        "z": "236bdcb1284426a5",
        "g": "1934884e6acaaf6c",
        "name": "INVERTER_STANDBY",
        "topic": "Sofar2mqtt/set/standby",
        "qos": "",
        "retain": "",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "3348075e0f83efab",
        "x": 3140,
        "y": 500,
        "wires": []
    },
    {
        "id": "792c83c59b62bdfc",
        "type": "change",
        "z": "236bdcb1284426a5",
        "g": "1934884e6acaaf6c",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "true",
                "tot": "str"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 2870,
        "y": 440,
        "wires": [
            [
                "9a482a2eba7531d1"
            ]
        ]
    },
    {
        "id": "701e199ff346c6ac",
        "type": "change",
        "z": "236bdcb1284426a5",
        "g": "1934884e6acaaf6c",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "true",
                "tot": "str"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 2870,
        "y": 500,
        "wires": [
            [
                "1731f716abe1bf2d"
            ]
        ]
    },
    {
        "id": "0462f0690349fd6d",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "30d526b71c5a89fc",
        "name": "MODE_AUTO_PISCINE_ON",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_on",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "input_boolean.gestion_piscine"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_on",
        "x": 2900,
        "y": 600,
        "wires": [
            []
        ]
    },
    {
        "id": "9dbfa8bafbdc3938",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "30d526b71c5a89fc",
        "name": "MODE_AUTO_PISCINE_OFF",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_off",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "input_boolean.gestion_piscine"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_off",
        "x": 2910,
        "y": 660,
        "wires": [
            []
        ]
    },
    {
        "id": "b6e8a65a855cc5ab",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "9f231d2df151524f",
        "name": "VMC_MODE_NORMAL",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "button.press",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "button.nodered_46e6315a2b659938"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "button",
        "service": "press",
        "x": 2890,
        "y": 760,
        "wires": [
            []
        ]
    },
    {
        "id": "cdf300da4292e6e7",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "9f231d2df151524f",
        "name": "VMC_MODE_BOOST",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "button.press",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "button.nodered_27029f3265930acc"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "button",
        "service": "press",
        "x": 2880,
        "y": 820,
        "wires": [
            []
        ]
    },
    {
        "id": "7b1641a88e3b768a",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "9f231d2df151524f",
        "name": "VMC MODE VACANCES",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "button.press",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "button.nodered_afd567bd657196bb"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "button",
        "service": "press",
        "x": 2890,
        "y": 880,
        "wires": [
            []
        ]
    },
    {
        "id": "b075d9b0eea63d81",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "e584f80d1f57904e",
        "name": "CHARGE_ON",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "button.press",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "button.lektrico_charge_start"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "button",
        "service": "press",
        "x": 2860,
        "y": 1040,
        "wires": [
            []
        ]
    },
    {
        "id": "f5061a132134d437",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "e584f80d1f57904e",
        "name": "CHARGE_OFF",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "button.press",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "button.lektrico_charge_stop"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "button",
        "service": "press",
        "x": 2860,
        "y": 980,
        "wires": [
            []
        ]
    },
    {
        "id": "c05179d91f2422d0",
        "type": "telegrambot-notify",
        "z": "236bdcb1284426a5",
        "name": "",
        "bot": "514978627925e867",
        "chatId": "-5086065964",
        "message": "",
        "parseMode": "",
        "x": 2180,
        "y": 260,
        "wires": []
    },
    {
        "id": "493a51d1d4a9e4ab",
        "type": "debug",
        "z": "236bdcb1284426a5",
        "name": "sortie gemini",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 910,
        "y": 60,
        "wires": []
    },
    {
        "id": "8e0e828a4e6da63d",
        "type": "debug",
        "z": "236bdcb1284426a5",
        "name": "debug 34",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "statusVal": "",
        "statusType": "auto",
        "x": 2160,
        "y": 200,
        "wires": []
    },
    {
        "id": "40ada44d0756d5bc",
        "type": "debug",
        "z": "236bdcb1284426a5",
        "name": "Depart",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 350,
        "y": 60,
        "wires": []
    },
    {
        "id": "5f9bc5b23a235478",
        "type": "debug",
        "z": "236bdcb1284426a5",
        "name": "sortie join",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 1360,
        "y": 60,
        "wires": []
    },
    {
        "id": "7e2dbd24a95ff56f",
        "type": "debug",
        "z": "236bdcb1284426a5",
        "name": "Vérification msg.commands",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "commands",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 2180,
        "y": 40,
        "wires": []
    },
    {
        "id": "7560dd048b1773f8",
        "type": "debug",
        "z": "236bdcb1284426a5",
        "name": "diviser",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 2690,
        "y": 40,
        "wires": []
    },
    {
        "id": "94560f551005daa5",
        "type": "change",
        "z": "236bdcb1284426a5",
        "name": "",
        "rules": [
            {
                "t": "move",
                "p": "commands",
                "pt": "msg",
                "to": "payload",
                "tot": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 2450,
        "y": 120,
        "wires": [
            [
                "e8e81d7f.c22d1"
            ]
        ]
    },
    {
        "id": "b72e16fc0ba11428",
        "type": "debug",
        "z": "236bdcb1284426a5",
        "name": "prompt rapport",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 1920,
        "y": 320,
        "wires": []
    },
    {
        "id": "fc072eb2d12bf155",
        "type": "debug",
        "z": "236bdcb1284426a5",
        "name": "prompt meteo",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 640,
        "y": 60,
        "wires": []
    },
    {
        "id": "5a33fe96bd718c34",
        "type": "json",
        "z": "236bdcb1284426a5",
        "name": "",
        "property": "payload",
        "action": "",
        "pretty": false,
        "x": 1130,
        "y": 120,
        "wires": [
            [
                "c3f2d1e2.abcdef"
            ]
        ]
    },
    {
        "id": "395643a3832c70e9",
        "type": "function",
        "z": "236bdcb1284426a5",
        "name": "Nettoyeur réponse gemini",
        "func": "// Ce noeud nettoie la réponse brute de Gemini et envoie un signal pour réinitialiser le timeout.\n\nlet rawResponse = msg.payload;\n\n// On vérifie que la réponse est bien une chaîne de caractères\nif (typeof rawResponse !== 'string') {\n    // Si ce n'est pas le cas, on la transforme en chaîne pour la suite\n    rawResponse = JSON.stringify(rawResponse);\n}\n\n// On utilise une expression régulière pour trouver la partie JSON.\nconst jsonMatch = rawResponse.match(/{[\\s\\S]*}/);\n\nif (jsonMatch && jsonMatch[0]) {\n    // Si on a trouvé une correspondance, on la met dans le payload\n    msg.payload = jsonMatch[0];\n} else {\n    // Sinon, on crée un objet d'erreur pour que le flux ne plante pas.\n    msg.payload = '{\"error\": \"Aucun objet JSON trouvé dans la réponse de Gemini\"}';\n}\n\n// On prépare le message de réinitialisation pour la deuxième sortie.\nconst resetMsg = { payload: \"reset\" };\n\n// On retourne le payload nettoyé sur la sortie 1, et le message de reset sur la sortie 2.\nreturn [msg, resetMsg];\n",
        "outputs": 2,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 930,
        "y": 120,
        "wires": [
            [
                "5a33fe96bd718c34"
            ],
            [
                "d3a3f5f5.a1b4e8"
            ]
        ]
    },
    {
        "id": "177b6a4071aa1684",
        "type": "function",
        "z": "236bdcb1284426a5",
        "name": "DISPACHEUR DE THERMOSTAT",
        "func": "// Ce noeud reçoit une commande climate (temp, on/off, ou mode)\nconst command = msg.payload;\n\n// --- Correspondances ---\nconst entityMap = {\n    'BUREAU': 'climate.bureau',\n    'SALON': 'climate.salon',\n    'CHAMBRE_PARENTS': 'climate.parents',\n    'CHAMBRE_LEA': 'climate.lea',\n    'CHAMBRE_CELIA': 'climate.celia'\n};\n\n// Traduction des modes pour Home Assistant\nconst modeMap = {\n    'CHAUFFAGE': 'heat',\n    'CLIMATISATION': 'cool',\n    'VENTILATION': 'fan_only'\n};\n\n// --- Test 1: Commande de Température ---\n// CORRIGÉ: Ajout de _TEMP_ pour capturer uniquement le nom de la pièce\nlet tempRegex = /^(.+)_TEMP_(\\d+\\.?\\d*)$/; \nlet match = command.match(tempRegex);\n\nif (match) {\n    const piece = match[1];\n    const temperature = parseFloat(match[2]);\n    const entityId = entityMap[piece];\n    if (entityId) {\n        msg.payload = {\n            type: \"call_service\", domain: \"climate\", service: \"set_temperature\",\n            service_data: { temperature: temperature },\n            target: { entity_id: entityId }\n        };\n        return msg;\n    }\n}\n\n// --- Test 2: Commande ON/OFF ---\n// CORRIGÉ: Ajout de THERMOSTAT_ et _ pour capturer uniquement le nom de la pièce\nlet onOffRegex = /^THERMOSTAT_(.+)_(ON|OFF)$/;\nmatch = command.match(onOffRegex);\n\nif (match) {\n    const piece = match[1];\n    const state = match[2];\n    const entityId = entityMap[piece];\n    const service = (state === 'ON') ? 'turn_on' : 'turn_off';\n    if (entityId) {\n        msg.payload = {\n            type: \"call_service\", domain: \"climate\", service: service,\n            target: { entity_id: entityId }\n        };\n        return msg;\n    }\n}\n\n// --- Test 3: Commande de Mode (Chauffage/Clim/Ventil) ---\n// CORRIGÉ: Ajout de _MODE_ pour capturer uniquement le nom de la pièce\nlet modeRegex = /^(.+)_MODE_(CHAUFFAGE|CLIMATISATION|VENTILATION)$/;\nmatch = command.match(modeRegex);\n\nif (match) {\n    const piece = match[1];\n    const mode = match[2];\n    const entityId = entityMap[piece];\n    const hvacMode = modeMap[mode]; // Traduit 'CHAUFFAGE' en 'heat', etc.\n    if (entityId && hvacMode) {\n        msg.payload = {\n            type: \"call_service\", domain: \"climate\", service: \"set_hvac_mode\",\n            service_data: { hvac_mode: hvacMode },\n            target: { entity_id: entityId }\n        };\n        return msg;\n    }\n}\n\n// Si la commande ne correspond à aucun test, on l'ignore.\nreturn null;\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 2940,
        "y": 120,
        "wires": [
            [
                "14694fc768b31de9",
                "1adc54b641b1403b"
            ]
        ]
    },
    {
        "id": "14694fc768b31de9",
        "type": "ha-api",
        "z": "236bdcb1284426a5",
        "name": "Appliquer Température Thermostat",
        "server": "bb0a3ead.a33a3",
        "version": 1,
        "debugenabled": false,
        "protocol": "websocket",
        "method": "get",
        "path": "",
        "data": "payload",
        "dataType": "jsonata",
        "responseType": "json",
        "outputProperties": [
            {
                "property": "payload",
                "propertyType": "msg",
                "value": "",
                "valueType": "results"
            }
        ],
        "x": 3280,
        "y": 120,
        "wires": [
            []
        ]
    },
    {
        "id": "1adc54b641b1403b",
        "type": "function",
        "z": "236bdcb1284426a5",
        "name": "Logger de thermostat",
        "func": "// Ce noeud reçoit le message formaté par le \"Dispatcheur\"\nconst entityId = msg.payload.target.entity_id;\nconst service = msg.payload.service;\nlet logMessage = \"\";\n\n// On adapte le message de log en fonction du service appelé\nif (service === 'set_temperature') {\nconst temperature = msg.payload.service_data.temperature;\nlogMessage = \"[Thermostat] Commande: Régler \" + entityId + \" sur \" + temperature + \"°C\";\n\n} else if (service === 'set_hvac_mode') {\nconst hvacMode = msg.payload.service_data.hvac_mode;\nlogMessage = \"[Thermostat] Commande: Activer mode '\" + hvacMode + \"' sur \" + entityId;\n\n} else { // C'est un service 'turn_on' ou 'turn_off'\nconst action = (service === 'turn_on') ? \"Allumer\" : \"Éteindre\";\nlogMessage = \"[Thermostat] Commande: \" + action + \" \" + entityId;\n}\n\n// On affiche le message dans le panneau de débogage\nnode.warn(logMessage);\n\nreturn null;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 3240,
        "y": 200,
        "wires": [
            []
        ]
    },
    {
        "id": "21e6c05cc60c61fe",
        "type": "function",
        "z": "236bdcb1284426a5",
        "name": "Prompt du Rapport",
        "func": "const payload = msg.payload;\nconst commands = msg.commands || [];\n\nconst prompt = `\nRédige un rapport quotidien concis et agréable à lire pour la gestion de la maison, en te basant **uniquement** sur les données suivantes.\n\n**Données Brutes :**\n- Mode saisonnier : ${payload.MODE_SAISONNIER}\n- Météo du jour : ${payload.METEO_JOUR}\n- Tarif Tempo : ${payload.COULEUR_TEMPO}\n- Commandes envoyées : ${commands.join(', ')}\n- Raison de l'ajustement de température : \"${payload.RAPPORT_AJUSTEMENT_TEMP}\"\n- Action décidée pour la batterie : \"${payload.RAPPORT_ACTION_BATTERIE}\"\n\n**Instructions de Formatage :**\nStructure ta réponse avec les sections suivantes :\n\n*   **Chauffage / Clim** : Décris les actions sur les thermostats. **Pour une meilleure lisibilité, regroupe les actions pour les chambres (Parents, Léa, Célia) et pour les pièces de vie (Bureau, Salon).** Si la raison de l'ajustement de température n'est pas \"Aucun\", mentionne-la.\n*   **Voiture Électrique** : Indique si la charge a été activée ou désactivée.\n*   **Piscine** : Indique l'état du mode automatique.\n*   **Batterie Maison** : Indique l'action exacte décidée (${payload.RAPPORT_ACTION_BATTERIE}).\n\n**Touche Finale :**\nTermine **toujours** le rapport par une citation inspirante ou une blague sur le thème de la domotique, la technologie ou la maison.\n`;\n\nmsg.payload = prompt;\n\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1610,
        "y": 300,
        "wires": [
            [
                "247c647c96be225d",
                "b72e16fc0ba11428"
            ]
        ]
    },
    {
        "id": "4db9e0618b1396b1",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "2f5e2fcfbe552773",
        "name": "PISCINE OFF",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_off",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "switch.shelly1minig3_84fce6382750",
            "switch.electrolyseur",
            "switch.lumiere_piscine"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_off",
        "x": 2860,
        "y": 1140,
        "wires": [
            []
        ]
    },
    {
        "id": "36388a7c171e859c",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "2f5e2fcfbe552773",
        "name": "PLANIKA ON",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_on",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "switch.planika_on_switch_1"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_on",
        "x": 2860,
        "y": 1220,
        "wires": [
            []
        ]
    },
    {
        "id": "0c2a1314b121cfe2",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "2f5e2fcfbe552773",
        "name": "LEKTRICO OFF",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_on",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "button.lektrico_charge_stop"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_on",
        "x": 2860,
        "y": 1260,
        "wires": [
            []
        ]
    },
    {
        "id": "f7056c1092e92d0b",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "2f5e2fcfbe552773",
        "name": "LUMIERE EXTERIEURE",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_off",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "switch.interupteur_lumiere_exterieure"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_off",
        "x": 2890,
        "y": 1180,
        "wires": [
            []
        ]
    },
    {
        "id": "0600826375f8dd75",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "2f5e2fcfbe552773",
        "name": "SECHE SERVIETTE OFF",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_on",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "switch.seche_serviette"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_on",
        "x": 2890,
        "y": 1300,
        "wires": [
            []
        ]
    },
    {
        "id": "f504f8535b3332cd",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "2f5e2fcfbe552773",
        "name": "PRISES BUREAU OFF",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_off",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "switch.e2m_prise_bureau_switch",
            "switch.prise_fontaine",
            "switch.0xa4c1386129e10bd2"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_off",
        "x": 2890,
        "y": 1420,
        "wires": [
            []
        ]
    },
    {
        "id": "06bb7e8c739fefe7",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "2f5e2fcfbe552773",
        "name": "PRISES SALON OFF",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_off",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "switch.prise_frigo",
            "switch.prise_enfant"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_off",
        "x": 2880,
        "y": 1500,
        "wires": [
            []
        ]
    },
    {
        "id": "bf050c8d7f1f56a9",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "2f5e2fcfbe552773",
        "name": "PRISE CHAMBRE LEA OFF",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_off",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "switch.e2m_prise_chambre_lea_switch"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_off",
        "x": 2900,
        "y": 1460,
        "wires": [
            []
        ]
    },
    {
        "id": "d4d45c71f888bbb8",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "2f5e2fcfbe552773",
        "name": "PRISE CHAMBRE PARENTS OFF",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_off",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "switch.prise_chambre_parents"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_off",
        "x": 2920,
        "y": 1340,
        "wires": [
            []
        ]
    },
    {
        "id": "6d2550589dc092a8",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "2f5e2fcfbe552773",
        "name": "PRISE TELESCOPE OFF",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_off",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "switch.0xa4c1386129e10bd2"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_off",
        "x": 2890,
        "y": 1380,
        "wires": [
            []
        ]
    },
    {
        "id": "23193f1cf1ab4828",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "911ce4bfeaf3c135",
        "name": "CHAUFFE EAU ON",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_on",
        "floorId": [],
        "areaId": [],
        "deviceId": [
            "2d866641304c892f7f95cd4126e3bf7f"
        ],
        "entityId": [],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_on",
        "x": 2880,
        "y": 1600,
        "wires": [
            []
        ]
    },
    {
        "id": "3dc9dc2f1ee7e481",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "911ce4bfeaf3c135",
        "name": "PRISES BUREAU ON",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_on",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "switch.e2m_prise_bureau_switch",
            "switch.prise_fontaine",
            "switch.0xa4c1386129e10bd2"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_on",
        "x": 2880,
        "y": 1640,
        "wires": [
            []
        ]
    },
    {
        "id": "567cfc1553529b90",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "911ce4bfeaf3c135",
        "name": "PRISE_VMC_ON",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_on",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "switch.prise_vmc"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_on",
        "x": 2870,
        "y": 1740,
        "wires": [
            [
                "18172beef3583059"
            ]
        ]
    },
    {
        "id": "f83056ad321e8412",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "911ce4bfeaf3c135",
        "name": "PRISE CHAMBRE PARENTS ON",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_on",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "switch.prise_chambre_parents"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_on",
        "x": 2920,
        "y": 1680,
        "wires": [
            []
        ]
    },
    {
        "id": "f082998a84a2efd6",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "911ce4bfeaf3c135",
        "name": "CODE_FONCTION_3",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "button.press",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "BOUTON_VMC_CODE_FONCTION_3"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "button",
        "service": "press",
        "x": 3260,
        "y": 1740,
        "wires": [
            []
        ]
    },
    {
        "id": "18172beef3583059",
        "type": "delay",
        "z": "236bdcb1284426a5",
        "g": "911ce4bfeaf3c135",
        "name": "",
        "pauseType": "delay",
        "timeout": "5",
        "timeoutUnits": "minutes",
        "rate": "1",
        "nbRateUnits": "1",
        "rateUnits": "second",
        "randomFirst": "1",
        "randomLast": "5",
        "randomUnits": "seconds",
        "drop": false,
        "allowrate": false,
        "outputs": 1,
        "x": 3060,
        "y": 1740,
        "wires": [
            [
                "f082998a84a2efd6"
            ]
        ]
    },
    {
        "id": "fc6b0e5a17a8d62b",
        "type": "gemini-api-key",
        "name": ""
    },
    {
        "id": "3348075e0f83efab",
        "type": "mqtt-broker",
        "name": "Home Assistant Mosquitto",
        "broker": "localhost",
        "port": "1883",
        "clientid": "HAClient",
        "autoConnect": true,
        "usetls": false,
        "protocolVersion": "4",
        "keepalive": "60",
        "cleansession": true,
        "autoUnsubscribe": true,
        "birthTopic": "",
        "birthQos": "0",
        "birthRetain": "false",
        "birthPayload": "",
        "birthMsg": {},
        "closeTopic": "",
        "closeQos": "0",
        "closePayload": "",
        "closeMsg": {},
        "willTopic": "",
        "willQos": "0",
        "willPayload": "",
        "willMsg": {},
        "userProps": "",
        "sessionExpiry": ""
    },
    {
        "id": "bb0a3ead.a33a3",
        "type": "server",
        "name": "Home Assistant",
        "addon": true
    },
    {
        "id": "514978627925e867",
        "type": "telegrambot-config",
        "botname": "NectiHome_bot",
        "usernames": "",
        "chatIds": "803094914",
        "pollInterval": 300
    },
    {
        "id": "b1962a31aaad72ca",
        "type": "global-config",
        "env": [],
        "modules": {
            "node-red-contrib-cron-plus": "2.2.3",
            "node-red-contrib-gemini": "1.0.2",
            "node-red-contrib-home-assistant-websocket": "0.80.3",
            "@danielnguyen/node-red-contrib-telegrambot-home": "0.5.3"
        }
    }
]

Salut !

Je suis également en cours de transition de Jeedom à Home Assistant, avec quelques différences par rapport à toi :

  • Je n’ai pas touché à Node Red, et ça ne m’attire pas vraiment. La logique a l’air très différente de ce à quoi je suis habitué (if then else). Les automatisations sont à peine plus simples, mais je demande aux LLM de me pondre du yaml correspondant
  • Je n’ai pas (encore ?) intégré de LLM directement dans HA

Quoi qu’il en soit, bravo pour ce boulot ! Je pense que ça ne sera pas réplicable facilement, mais ça montre les possibilités !
Mais bon, je n’ai ni voiture électrique, ni batterie. Et seulement 2 petits panneaux pour absorber ma conso talon

Je n’ai peut être pas tout lu en détail, ou j’ai du le rater, mais pour la régularisation de la température une fois la consigne fournie, tu t’appuies sur quoi ?
Personnellement j’ai du Versatile qui pilote les différents systèmes de chauffage (PAC via esp32, grille pain électrique, poele à granulé via Netatmo), et je joue avec des Schedulers différents en fonction du jour rouge ou non (qui change les horaires et les consignes de température). Chez toi tu délègues tout ça à l’IA finalement ?!

PS : j’ai même créé mon compte sur ce forum pour t’adresser mes félicitations. Ca sera fait pour de futurs posts :smiley:

Merci pour ton message, pour la régulation de la température, avec l’intégration AIRZONE, j’ai directement accés a tous mes thermostats, quir régulent pièce par pièces.

J’ai ajouté un ajustement dynamique également, en fonction de la température extérieure, qui va ajouter un ou plusieurs degrés (tout est parametrable a l’aide de helper (variables), que je définis manuellement depuis mon dashboard).

Je comprend pour Nodered, tu as egalement CAFE qui est sorti récément via HACS, qui permet de donner une interface visuelle de type nodered aux automatisations, l’avantage c’est que ca te crée des automatisation en YAML directement, qui fonctionnent meme si tu désinstalle l’intégration.

Bon courage pour la migration.

J’oubliais, j’en suis à la V8 actuellement, et tout fonctionne en local, si des personnes sont intéressées, je peux uploader le code complet.

Bon allé je me motive, ca servira surement a quelques personnes, je partage directement ma note de backup:
Pour info, je ne suis plus tempo, du coup mes heures pleines/creuses ont changé, par contre, le flux est compatible avec les deux options via helpers dédiés.

Changelog - 13 janvier 2026

  • [FIX] Correction rapport quotidien vide : Variables individuelles créées dans nœud PRÉPARATION au lieu de RAPPORT_QUOTIDIEN unique
  • [NEW] Gestion présence différenciée par personne (YOHAN_SEUL, JENNY_SEULE, COUPLE, MIXTE) avec chauffage adapté
  • [NEW] Pause déjeuner 12h-13h30 : Salon + Bureau en confort pour Jenny
  • [CHANGE] Retour mode normal le soir à 17h30 au lieu de 18h
  • [ENHANCE] Rapport enrichi : section présence avec émojis (:bust_in_silhouette::woman::busts_in_silhouette:), 19 phrases de conclusion, underscores supprimés

Changelog - 22 janvier 2026 (V8)

  • [NEW] Intégration d’une logique double fournisseur pour basculer entre les stratégies Tempo et Novalix via un helper.
  • [NEW] Développement d’une stratégie d’optimisation spécifique pour Novalix, exploitant les deux plages d’heures creuses (nuit et après-midi).
  • [NEW] Ajout de 3 helpers dédiés au pilotage de la stratégie tarifaire :
    • input_select.mode_fournisseur_electrique
    • input_number.novalix_seuil_recharge_aprem
    • input_number.novalix_surconfort_hc

1. Objectif Général

Ce flux automatise la gestion énergétique et le confort de la maison en se basant sur la saison, l’heure, la présence des occupants, le schéma tarifaire sélectionné (Tempo ou Novalix) et la météo. Il permet de basculer dynamiquement entre deux logiques d’optimisation distinctes pour s’adapter parfaitement au contrat d’électricité en vigueur.

Le cœur du système est une logique de température dynamique qui ajuste intelligemment les consignes de confort en fonction de la production solaire, de la consommation électrique et des températures extérieures extrêmes pour optimiser les coûts et le confort.

Depuis la V3, le flux intègre une gestion avancée de la batterie domestique, incluant des modes de charge/décharge automatiques, des modes forcés pour la maintenance (comme l’équilibrage), et une anticipation des jours Tempo Rouges pour maximiser les économies.

Chaque jour, le flux génère des commandes pour Home Assistant, met à jour des entités input_text pour un suivi en temps réel des états, et produit un rapport quotidien fiable et synthétique.

2. Nouveautés de la V4

  • Gestion Intelligente de la Batterie : Le flux peut désormais piloter l’onduleur en mode automatique (basé sur Tempo, météo) ou en mode forcé (Charge, Décharge, Standby, et Equalization pour la maintenance).
  • Anticipation des Jours Rouges : Le système détecte la veille au soir si le lendemain est un jour Tempo Rouge et peut déclencher des actions préventives (pré-chauffage de la maison, charge forcée de la batterie).
  • Consigne de Charge Voiture Flexible : Le seuil de charge de la voiture électrique est maintenant contrôlé par un input_number dans Home Assistant.
  • Fiabilisation de l’État : La gestion du forçage manuel des thermostats a été externalisée dans des helpers Home Assistant (input_text et input_number), rendant le système plus robuste aux redémarrages.

Nouveautés de la V8 : Flexibilité et Double Stratégie

  • Sélecteur de Fournisseur : Le cœur de cette mise à jour est l’ajout d’un helper input_select permettant de choisir entre les modes « Tempo » et « Novalix ». L’ensemble du flux adapte désormais sa logique en fonction de ce choix, rendant le système agnostique au fournisseur.
  • Stratégie Novalix Intelligente : Une logique entièrement nouvelle a été développée pour tirer parti des heures creuses réparties de Novalix (01h-07h et 14h30-16h30). Elle optimise la charge de la batterie en deux temps et utilise l’inertie thermique du bâtiment en pré-chauffant durant les heures creuses.
  • Nouveaux Helpers de Pilotage : Trois nouveaux helpers ont été créés pour affiner la stratégie tarifaire, notamment le seuil de recharge de la batterie l’après-midi et l’intensité du « sur-confort » thermique pendant les heures creuses.

3. Acteurs Principaux

  • Home Assistant : Fournit les états des capteurs (y compris les prévisions météo), les réglages (helpers) et exécute les commandes finales.
  • Node-RED : Contient toute la logique de décision, répartie en plusieurs nœuds spécialisés.

4. Liste des Helpers Requis

Tous ces helpers doivent être créés dans Home Assistant pour que le flux fonctionne.

  • Températures :

    • input_number.temp_confort_hiver (ex: 20)
    • input_number.temp_eco_nuit_hiver (ex: 18)
    • input_number.temp_confort_ete (ex: 24)
    • input_number.temp_eco_nuit_ete (ex: 26)
    • input_number.temp_hors_gel (ex: 16)
    • input_number.temp_eco_jour_rouge
  • Horaires :

    • input_boolean.activation_prechauffage_matin
    • input_number.heure_debut_prechauffage_matin (ex: 5)
    • input_number.heure_fin_prechauffage_matin (ex: 7)
    • input_boolean.activation_prechauffage_soir
    • input_number.heure_debut_prechauffage_soir (ex: 20)
    • input_number.heure_fin_prechauffage_soir (ex: 22)
    • input_boolean.ventilation_auto_mi_saison
    • input_number.heure_debut_ventilation_mi_saison (ex: 10)
    • input_number.heure_fin_ventilation_mi_saison (ex: 14)
  • Voiture Électrique :

    • input_number.seuil_surplus_solaire_voiture (ex: 1000)
    • input_number.voiture_consigne_charge (ex: 80)
    • Batterie Domestique :
      • input_boolean.gestion_automatique_batterie
      • input_select.batterie_maison_mode_force (Options: Charge, Discharge, Standby, Auto, Equalization)
      • input_number.seuil_batterie_auto_solaire_nuit (ex: 80)
  • Gestion Dynamique :

    • input_boolean.gestion_dynamique_conso_elec

    • input_number.seuil_modulation_eco (ex: -2000)

    • input_number.seuil_boost_solaire (ex: 1500)

    • input_boolean.gestion_dynamique_temp_exterieure

    • input_number.ajustement_temp_dynamique (ex: 1.0)

    • input_number.seuil_froid_exterieur (ex: 0)

    • input_number.seuil_chaud_exterieur (ex: 35)

    • input_number.seuil_temp_max_hiver (ex: 14)

    • input_number.seuil_temp_max_ete (ex: 23)

    • input_number.hysteresis_saison (ex: 2)

  • Général / Forçage :

    • input_number.duree_forcage_manuel (en heures, ex: 3)
    • input_number.heure_rapport_quotidien (ex: 12)
    • input_number.minute_rapport_quotidien (ex: 30)
    • input_boolean.gestion_auto_bureau
    • input_boolean.gestion_auto_salon
    • input_boolean.gestion_auto_ch_parents
    • input_boolean.gestion_auto_ch_lea
    • input_boolean.gestion_auto_ch_celia
    • input_boolean.rapports_maison (mode debug)
    • input_boolean.voiture_charge_forcee
  • État Interne Externalisé (Très important) :

    • input_text.override_timestamp_bureau
    • input_text.override_timestamp_salon
    • input_text.override_timestamp_chambre_parents
    • input_text.override_timestamp_chambre_lea
    • input_text.override_timestamp_chambre_celia
    • input_number.last_temp_set_bureau
    • input_number.last_temp_set_salon
    • input_number.last_temp_set_chambre_parents
    • input_number.last_temp_set_chambre_lea
    • input_number.last_temp_set_chambre_celia
  • Textes d’État (pour affichage dans le tableau de bord) :

    • input_text.mode_temperature
    • input_text.prechauffage
    • input_text.gestion_dynamique
    • input_text.marche_forcee
    • input_text.gestion_auto
    • input_text.utilisation_temp_exterieure
    • input_text.mi_saison
    • input_number.seuil_batterie_auto_solaire_nuit
      input_number.hysteresis_saison

Parfait ! Vous avez raison, restons simples et efficaces. Voici la liste complète des 9 helpers à créer dans Home Assistant :

:clipboard: LISTE DES HELPERS À CRÉER

:one: Activation/Désactivation de la gestion différenciée

input_boolean.gestion_presence_differentielle
  name: "Gestion présence différenciée"
  icon: mdi:account-switch

Rôle : Active ou désactive complètement la gestion par personne. Si OFF → comportement normal pour tout le monde.


:two: Pause déjeuner - Heure de début

input_number.heure_debut_pause_dejeuner
  name: "Pause déjeuner - Heure début"
  min: 0
  max: 23
  step: 1
  initial: 12
  unit_of_measurement: "h"
  icon: mdi:clock-start

:three: Pause déjeuner - Minute de début

input_number.minute_debut_pause_dejeuner
  name: "Pause déjeuner - Minute début"
  min: 0
  max: 59
  step: 1
  initial: 0
  unit_of_measurement: "min"
  icon: mdi:clock-start

:four: Pause déjeuner - Heure de fin

input_number.heure_fin_pause_dejeuner
  name: "Pause déjeuner - Heure fin"
  min: 0
  max: 23
  step: 1
  initial: 13
  unit_of_measurement: "h"
  icon: mdi:clock-end

:five: Pause déjeuner - Minute de fin

input_number.minute_fin_pause_dejeuner
  name: "Pause déjeuner - Minute fin"
  min: 0
  max: 59
  step: 1
  initial: 30
  unit_of_measurement: "min"
  icon: mdi:clock-end

:six: Retour mode normal - Heure

input_number.heure_retour_mode_normal
  name: "Retour mode normal - Heure"
  min: 0
  max: 23
  step: 1
  initial: 17
  unit_of_measurement: "h"
  icon: mdi:home-clock

:seven: Retour mode normal - Minute

input_number.minute_retour_mode_normal
  name: "Retour mode normal - Minute"
  min: 0
  max: 59
  step: 1
  initial: 30
  unit_of_measurement: "min"
  icon: mdi:home-clock

:eight: Jours d’activation de la gestion différenciée

input_select.jours_gestion_differentielle
  name: "Jours gestion différenciée"
  options:
    - "Lundi au Vendredi"
    - "Lundi au Samedi"
    - "Tous les jours"
    - "Personnalisé"
  initial: "Lundi au Vendredi"
  icon: mdi:calendar-week

:nine: Jours personnalisés (optionnel - si « Personnalisé » sélectionné)

input_boolean.gestion_diff_lundi
  name: "Gestion diff. - Lundi"
  icon: mdi:calendar

input_boolean.gestion_diff_mardi
  name: "Gestion diff. - Mardi"
  icon: mdi:calendar

input_boolean.gestion_diff_mercredi
  name: "Gestion diff. - Mercredi"
  icon: mdi:calendar

input_boolean.gestion_diff_jeudi
  name: "Gestion diff. - Jeudi"
  icon: mdi:calendar

input_boolean.gestion_diff_vendredi
  name: "Gestion diff. - Vendredi"
  icon: mdi:calendar

input_boolean.gestion_diff_samedi
  name: "Gestion diff. - Samedi"
  icon: mdi:calendar

input_boolean.gestion_diff_dimanche
  name: "Gestion diff. - Dimanche"
  icon: mdi:calendar

:bar_chart: RÉSUMÉ

Helper Type Valeur par défaut Description
gestion_presence_differentielle Boolean ON Active/désactive la gestion différenciée
heure_debut_pause_dejeuner Number 12 Heure début pause (12h)
minute_debut_pause_dejeuner Number 0 Minute début pause (00)
heure_fin_pause_dejeuner Number 13 Heure fin pause (13h)
minute_fin_pause_dejeuner Number 30 Minute fin pause (30)
heure_retour_mode_normal Number 17 Heure retour normal (17h)
minute_retour_mode_normal Number 30 Minute retour normal (30)
jours_gestion_differentielle Select Lun-Ven Jours actifs
gestion_diff_[jour] (×7) Boolean - Jours personnalisés

Total : 9 helpers principaux + 7 optionnels = 16 helpers maximum

input_boolean.gestion_diff_lundi
  name: "Gestion diff. - Lundi"
  icon: mdi:calendar

input_boolean.gestion_diff_mardi
  name: "Gestion diff. - Mardi"
  icon: mdi:calendar

input_boolean.gestion_diff_mercredi
  name: "Gestion diff. - Mercredi"
  icon: mdi:calendar

input_boolean.gestion_diff_jeudi
  name: "Gestion diff. - Jeudi"
  icon: mdi:calendar

input_boolean.gestion_diff_vendredi
  name: "Gestion diff. - Vendredi"
  icon: mdi:calendar

input_boolean.gestion_diff_samedi
  name: "Gestion diff. - Samedi"
  icon: mdi:calendar

input_boolean.gestion_diff_dimanche
  name: "Gestion diff. - Dimanche"
  icon: mdi:calendar


Nouveaux Helpers pour la Stratégie Tarifaire (V8) Ces helpers sont essentiels pour piloter la nouvelle logique double fournisseur. #### 1. Sélecteur de Fournisseur (Obligatoire)

mode_fournisseur_electrique: 
name: "Mode Fournisseur Électrique" 
options: 
- "Tempo" 
  - "Novalix" 
    initial: "Novalix" 
    icon: mdi:transmission-tower
input_number:
  novalix_seuil_recharge_aprem:
    name: "Novalix - Seuil SOC Recharge Après-midi"
    min: 20
    max: 90
    step: 5
    initial: 70
    unit_of_measurement: "%"
    icon: mdi:battery-arrow-down-outline

5. Déroulement du Flux

  1. Récupération Météo : Le flux appelle le service weather.get_forecasts de Home Assistant.
  2. Nœud « Validation Météo » : S’assure que les données météo sont correctement formatées.
  3. Nœud « Variables de Contexte » : Collecte l’état de tous les capteurs et helpers de Home Assistant.
  4. Chaîne de Logique Séquentielle :
    • Logique Batterie : Gère le mode de la batterie domestique (auto ou forcé).
    • Logique Thermostats : Calcule les consignes de température et gère les forçages manuels.
    • Logique Véhicule : Décide s’il faut activer ou non la charge de la voiture.
  5. Nœud « Préparation Rapport et Finalisation » : Assemble les phrases pour le rapport, met à jour les input_text d’état et génère deux messages de sortie.
  6. Division du Flux :
    • Branche 1 (Actions) : Reçoit la liste des commandes (msg.commands) à exécuter.
    • Branche 2 (Rapport) : Reçoit les données pour le rapport quotidien.
  7. Filtre et Formatage : Le rapport est filtré par heure, puis formaté en Markdown.
  8. Notification : Le rapport final est envoyé.

Stratégie « Hiver » (Conservation d’énergie)

  • Période : Lorsque MODE_SAISONNIER est HIVER ou MI-SAISON-HIVER.
  • Objectif : Avoir une batterie pleine chaque matin en profitant des heures creuses, car la production solaire de la journée sera probablement insuffisante pour couvrir les besoins ET recharger la batterie.
  • Logique Nocturne (22h-06h) :
    • Action : Forcer la CHARGE de la batterie (PACK_BATTERIE_CHARGE) pour la remplir avec l’électricité la moins chère possible.

Nouvelle Stratégie « Solaire » (Optimisation de l’autoconsommation)

  • Période : Lorsque MODE_SAISONNIER est ÉTÉ ou MI-SAISON-ETE.

  • Objectif : Maximiser l’absorption de l’énergie solaire du lendemain en s’assurant que la batterie n’est pas déjà pleine au lever du soleil.

  • Logique Nocturne (22h-06h) : La décision se base sur la prévision météo du lendemain.

    • Cas 1 : Demain sera une journée ensoleillée (METEO_DEMAIN = ‹ BONNE ›)

      • Action : On ne charge PAS la batterie. On la laisse en mode AUTO pour qu’elle se décharge naturellement en alimentant la consommation de la maison pendant la nuit.
      • Résultat attendu : Au lever du soleil, la batterie est partiellement vide, prête à stocker toute la production solaire de la journée.
    • Cas 2 : Demain sera une journée nuageuse/pluvieuse (METEO_DEMAIN = ‹ MAUVAISE ›)

      • Action : On anticipe une faible production solaire. On se comporte donc comme en hiver : on profite des heures creuses pour CHARGER la batterie.
      • Résultat attendu : Au lever du soleil, la batterie est pleine, prête à affronter une journée sans soleil.
        Votre idée d’ajouter une condition de production solaire minimale est la solution parfaite. Nous allons donc modifier le flux pour que, les jours rouges ensoleillés, le chauffage ne s’active que si la production solaire est supérieure à 2000W.

Règle Prioritaire (inchangée)

  • Indépendamment de la saison, si la veille au soir on détecte que le lendemain est un jour Rouge, on force la CHARGE de la batterie pendant la nuit pour être prêt.

Cette stratégie à deux modes, pilotée par la saison et la météo du lendemain, est exactement ce qu’il faut. Maintenant que nous sommes alignés, voici les blocs de code complets à modifier.

Stratégie « Novalix » (Optimisation Heures Creuses Réparties)
Lorsque le mode « Novalix » est sélectionné, l’objectif n’est plus d’éviter les jours rouges, mais de maximiser l’utilisation des deux plages d’heures creuses (01h-07h et 14h30-16h30) tout en priorisant l’autoconsommation solaire.

  • Batterie Domestique : - HC Nuit (01h-07h) : Charge forcée pour garantir une batterie à 100% au début de la journée.
  • HC Après-midi (14h30-16h30) : Opportunité de « complément ». Si le SOC est passé sous le seuil défini par input_number.novalix_seuil_recharge_aprem (à cause d’une faible production solaire le matin), une nouvelle charge est lancée.
  • Heures Pleines : La batterie est en mode AUTO, se déchargeant pour couvrir les besoins de la maison et se rechargeant uniquement avec le surplus solaire.
  • Thermostats (Chauffage/Clim) :
  • Pendant les Heures Creuses (Nuit et Après-midi) : La consigne de confort est augmentée de la valeur de input_number.novalix_surconfort_hc (ex: +0.5°C). Cela permet de stocker de l’énergie thermique (inertie) dans la maison à bas coût.
  • Pendant les Heures Pleines : La consigne de confort standard est appliquée. La logique de modulation dynamique (boost solaire) reste active pour profiter de l’énergie gratuite.
  • Véhicule Électrique : - La charge est autorisée en priorité avec le surplus solaire, puis pendant les HC de Nuit, et enfin pendant les HC d’Après-midi si un appoint est nécessaire.

6. Code des Nœuds (Version 4)

CODE DU NŒUD : API MÉTÉO

//PROTOCOL websocket msg.forecast =results
{
    "type": "call_service",
    "domain": "weather",
    "service": "get_forecasts",
    "return_response": true,
    "target": {
        "entity_id": "weather.forecast_maison"
    },
    "service_data": {
        "type": "daily"
    }
}

CODE DU NŒUD : VALIDATION MÉTÉO (Fonction)

// ### CODE DU NŒUD : VALIDATION MÉTÉO (Fonction) - AMÉLIORÉ ###

try {
    // Ce noeud reçoit le résultat de l'API météo.
    // D'après les logs, les données de prévision sont sous msg.forecast_data.response.weather.forecast_maison.forecast

    let weatherForecast = {}; // Initialise un objet vide par défaut

    // Vérifie si msg.forecast_data.response et les données de prévision sont présentes
    if (msg.forecast_data && msg.forecast_data.response && msg.forecast_data.response['weather.forecast_maison'] && msg.forecast_data.response['weather.forecast_maison'].forecast) {
        const forecast = msg.forecast_data.response['weather.forecast_maison'].forecast;
        
        // AMÉLIORATION : Validation du format des prévisions (au moins 2 jours nécessaires)
        if (Array.isArray(forecast) && forecast.length >= 2) {
            weatherForecast = msg.forecast_data.response['weather.forecast_maison'];
        } else {
            node.warn(`Format de prévisions météo invalide : ${forecast ? forecast.length : 0} jours disponibles (minimum 2 requis).`);
        }
    } else {
        node.warn("L'appel API météo a échoué ou a expiré. Continuation avec des données météo vides.");
    }

    // Place les données de prévision (ou l'objet vide) dans msg.forecast_data
    msg.forecast_data = weatherForecast;

    // Supprime les propriétés originales de l'API pour nettoyer le message
    delete msg.payload;
    delete msg.response;
    delete msg.topic;

    return msg;

} catch (e) {
    node.error("Erreur lors de la validation des données météo : " + e.message, msg);
    // Retourne un message avec des données météo vides pour permettre au flux de continuer
    msg.forecast_data = {};
    delete msg.payload;
    delete msg.response;
    delete msg.topic;
    return msg;
}

**CODE DU NŒUD : VARIABLES DE CONTEXTE (Fonction)**

// ============================================================

// NŒUD : VARIABLES DE CONTEXTE - VERSION V8 NOVALIX

// Type: Function

// ============================================================

// Ce nœud récupère tous les états de Home Assistant et construit

// le payload qui sera utilisé par les nœuds de logique.

//

// ✅ NOUVEAUTÉS V8 :

// - Ajout des 3 helpers Novalix

// - Calcul des plages horaires HC Novalix

// - Support double fournisseur (Tempo + Novalix)

// ============================================================

  

// Helper pour trouver une entité dans le tableau et retourner son état

function getState(entities, entityId) {

    const entity = entities.find(e => e.entity_id === entityId);

    if (!entity) {

        node.warn(`Entité non trouvée : ${entityId}`);

        return null;

    }

    if (entityId.startsWith('input_number') || entityId.startsWith('sensor')) {

        const num = parseFloat(entity.state);

        return isNaN(num) ? entity.state : num;

    }

    if (entityId.startsWith('input_boolean') || entityId.startsWith('binary_sensor')) {

        return entity.state === 'on';

    }

    if (entityId.startsWith('input_select')) {

        return entity.state;

    }

    return entity.state;

}

  

// Helper pour obtenir un attribut spécifique d'une entité

function getAttribute(entities, entityId, attribute) {

    const entity = entities.find(e => e.entity_id === entityId);

    if (!entity || !entity.attributes || typeof entity.attributes[attribute] === 'undefined') {

        node.warn(`Attribut '${attribute}' non trouvé pour l'entité : ${entityId}`);

        return null;

    }

    return entity.attributes[attribute];

}

  

const now = new Date();

const currentMonth = now.getMonth(); // 0 for January, 11 for December

const currentDay = now.getDay(); // 0 = Dimanche, 1 = Lundi, ..., 6 = Samedi

  

// --- 1. Récupérer les données météo du message précédent (VALIDATION MÉTÉO) ---

const forecastData = msg.forecast_data;

  

// --- 2. Récupérer les états de toutes les entités Home Assistant ---

const entityIds = [

    'sensor.rte_tempo_couleur_actuelle',

    'sensor.rte_tempo_prochaine_couleur',

    'sensor.date_time',

    'weather.vergeze',

    'sensor.presence_etat',

    'alarm_control_panel.alarmo',

    'sensor.presence_personnes',

    'sensor.sofar_battery_soc',

    'sensor.sofar_grid_power',

    'sensor.sofar_solar_pv_generation',

    'sensor.volvo_xc40_batterie',

    'input_number.temp_confort_hiver',

    'input_number.temp_eco_nuit_hiver',

    'input_number.temp_confort_ete',

    'input_number.temp_eco_nuit_ete',

    'input_number.temp_hors_gel',

    'input_number.temp_eco_jour_rouge',

    'input_boolean.activation_prechauffage_matin',

    'input_number.heure_debut_prechauffage_matin',

    'input_number.heure_fin_prechauffage_matin',

    'input_boolean.activation_prechauffage_soir',

    'input_number.heure_debut_prechauffage_soir',

    'input_number.heure_fin_prechauffage_soir',

    'input_boolean.gestion_auto_bureau',

    'input_boolean.gestion_auto_salon',

    'input_boolean.gestion_auto_ch_parents',

    'input_boolean.gestion_auto_ch_lea',

    'input_boolean.gestion_auto_ch_celia',

    'input_boolean.ventilation_auto_mi_saison',

    'input_number.heure_debut_ventilation_mi_saison',

    'input_number.heure_fin_ventilation_mi_saison',

    'input_number.ajustement_temp_dynamique',

    'input_number.seuil_modulation_eco',

    'input_number.seuil_boost_solaire',

    'input_number.seuil_froid_exterieur',

    'input_number.seuil_chaud_exterieur',

    'input_number.seuil_batterie_auto_solaire_nuit',

    'climate.bureau',

    'climate.salon',

    'climate.parents',

    'climate.lea',

    'climate.celia',

    'input_number.duree_forcage_manuel',

    'input_text.override_timestamp_bureau',

    'input_text.override_timestamp_salon',

    'input_text.override_timestamp_chambre_parents',

    'input_text.override_timestamp_chambre_lea',

    'input_text.override_timestamp_chambre_celia',

    'input_number.last_temp_set_bureau',

    'input_number.last_temp_set_salon',

    'input_number.last_temp_set_chambre_parents',

    'input_number.last_temp_set_chambre_lea',

    'input_number.last_temp_set_chambre_celia',

    'input_boolean.gestion_automatique_batterie',

    'input_select.batterie_maison_mode_force',

    'input_boolean.voiture_charge_forcee',

    'input_number.voiture_consigne_charge',

    'input_number.seuil_surplus_solaire_voiture',

    'input_boolean.rapports_maison',

    'input_number.heure_rapport_quotidien',

    'input_number.minute_rapport_quotidien',

    'input_boolean.gestion_dynamique_conso_elec',

    'input_boolean.gestion_dynamique_temp_exterieure',

    'input_number.seuil_temp_max_hiver',

    'input_number.seuil_temp_max_ete',

    'input_number.hysteresis_saison',

    // Helpers de gestion différenciée

    'input_boolean.gestion_presence_differentielle',

    'input_number.heure_debut_pause_dejeuner',

    'input_number.minute_debut_pause_dejeuner',

    'input_number.heure_fin_pause_dejeuner',

    'input_number.minute_fin_pause_dejeuner',

    'input_number.heure_retour_mode_normal',

    'input_number.minute_retour_mode_normal',

    'input_select.jours_gestion_differentielle',

    'input_boolean.gestion_diff_lundi',

    'input_boolean.gestion_diff_mardi',

    'input_boolean.gestion_diff_mercredi',

    'input_boolean.gestion_diff_jeudi',

    'input_boolean.gestion_diff_vendredi',

    'input_boolean.gestion_diff_samedi',

    'input_boolean.gestion_diff_dimanche',

    'input_boolean.suivi_presence_yohan',

    'input_boolean.suivi_presence_jenny',

    'input_boolean.suivi_presence_invite',

    // ✅ NOUVEAUX HELPERS V8 NOVALIX

    'input_select.mode_fournisseur_electrique',

    'input_number.novalix_seuil_recharge_aprem',

    'input_number.novalix_surconfort_hc'

];

  

const allStates = global.get('homeassistant.homeAssistant.states');

  

if (!allStates) {

    node.error("Le contexte global de Home Assistant n'est pas disponible. Vérifiez la connexion de l'intégration Node-RED.", msg);

    return null;

}

  

const haEntities = [];

entityIds.forEach(entityId => {

    if (allStates[entityId]) {

        haEntities.push(allStates[entityId]);

    } else {

        node.warn(`L'entité "${entityId}" n'a pas été trouvée dans le contexte global de Home Assistant.`);

    }

});

  

if (haEntities.length === 0) {

    node.error("Aucune des entités spécifiées n'a été trouvée. Le flux s'arrête.", msg);

    return null;

}

  

// --- Logique de présence ---

const presenceEtatHA = getState(haEntities, 'sensor.presence_etat') || 'ABSENT';

const presencePersonnesHA = getState(haEntities, 'sensor.presence_personnes') || 'Personne';

const alarmeState = getState(haEntities, 'alarm_control_panel.alarmo') || 'disarmed';

const alarmeActivee = alarmeState !== 'disarmed';

  

const suiviYohan = getState(haEntities, 'input_boolean.suivi_presence_yohan') !== false;

const suiviJenny = getState(haEntities, 'input_boolean.suivi_presence_jenny') !== false;

const suiviInvite = getState(haEntities, 'input_boolean.suivi_presence_invite') !== false;

  

let personnesDetectees = [];

if (presencePersonnesHA && presencePersonnesHA !== 'Personne') {

    personnesDetectees = presencePersonnesHA.split(', ').map(name => name.trim());

}

  

let personnesPresentes = [];

personnesDetectees.forEach(personne => {

    if (personne === 'YOHAN' && suiviYohan) personnesPresentes.push('YOHAN');

    else if (personne === 'JENNY' && suiviJenny) personnesPresentes.push('JENNY');

    else if (personne === 'INVITE' && suiviInvite) personnesPresentes.push('INVITE');

});

  

const maisonInhabitee = (personnesPresentes.length === 0) || alarmeActivee;

  

let profilPresence = 'ABSENT';

const isYohanPresent = personnesPresentes.includes('YOHAN');

const isJennyPresente = personnesPresentes.includes('JENNY');

const isInvitePresent = personnesPresentes.includes('INVITE');

  

if (maisonInhabitee) {

    profilPresence = 'ABSENT';

} else if (isYohanPresent && isJennyPresente) {

    profilPresence = 'COUPLE';

} else if (isYohanPresent && !isJennyPresente && !isInvitePresent) {

    profilPresence = 'YOHAN_SEUL';

} else if (isJennyPresente && !isYohanPresent && !isInvitePresent) {

    profilPresence = 'JENNY_SEULE';

} else if (isInvitePresent && !isYohanPresent && !isJennyPresente) {

    profilPresence = 'INVITE_SEUL';

} else {

    profilPresence = 'MIXTE';

}

  

// --- Calcul du surplus de puissance ---

const puissanceReseau = getState(haEntities, 'sensor.sofar_grid_power') || 0;

const surplusPuissance = (puissanceReseau < 0) ? -puissanceReseau : 0;

  

// --- Logique Météo du jour ---

const meteoJourState = getState(haEntities, 'weather.vergeze');

const bonneMeteoStates = ['sunny', 'partlycloudy', 'clear-night'];

const meteoJour = bonneMeteoStates.includes(meteoJourState) ? 'BONNE' : 'MAUVAISE';

  

// --- Gestion différenciée ---

const gestionPresenceDifferentielle = getState(haEntities, 'input_boolean.gestion_presence_differentielle') || false;

const heureDebutPauseDejeuner = getState(haEntities, 'input_number.heure_debut_pause_dejeuner') || 12;

const minuteDebutPauseDejeuner = getState(haEntities, 'input_number.minute_debut_pause_dejeuner') || 0;

const heureFinPauseDejeuner = getState(haEntities, 'input_number.heure_fin_pause_dejeuner') || 13;

const minuteFinPauseDejeuner = getState(haEntities, 'input_number.minute_fin_pause_dejeuner') || 30;

const heureRetourModeNormal = getState(haEntities, 'input_number.heure_retour_mode_normal') || 17;

const minuteRetourModeNormal = getState(haEntities, 'input_number.minute_retour_mode_normal') || 30;

const joursGestionDifferentielle = getState(haEntities, 'input_select.jours_gestion_differentielle') || 'Tous les jours';

  

const gestionDiffLundi = getState(haEntities, 'input_boolean.gestion_diff_lundi') !== false;

const gestionDiffMardi = getState(haEntities, 'input_boolean.gestion_diff_mardi') !== false;

const gestionDiffMercredi = getState(haEntities, 'input_boolean.gestion_diff_mercredi') !== false;

const gestionDiffJeudi = getState(haEntities, 'input_boolean.gestion_diff_jeudi') !== false;

const gestionDiffVendredi = getState(haEntities, 'input_boolean.gestion_diff_vendredi') !== false;

const gestionDiffSamedi = getState(haEntities, 'input_boolean.gestion_diff_samedi') !== false;

const gestionDiffDimanche = getState(haEntities, 'input_boolean.gestion_diff_dimanche') !== false;

  

let gestionDiffActiveAujourdhui = false;

if (gestionPresenceDifferentielle) {

    if (joursGestionDifferentielle === 'Tous les jours') {

        gestionDiffActiveAujourdhui = true;

    } else if (joursGestionDifferentielle === 'Lundi au Vendredi') {

        gestionDiffActiveAujourdhui = (currentDay >= 1 && currentDay <= 5);

    } else if (joursGestionDifferentielle === 'Lundi au Samedi') {

        gestionDiffActiveAujourdhui = (currentDay >= 1 && currentDay <= 6);

    } else if (joursGestionDifferentielle === 'Personnalisé') {

        const joursBooleans = [

            gestionDiffDimanche, gestionDiffLundi, gestionDiffMardi, gestionDiffMercredi,

            gestionDiffJeudi, gestionDiffVendredi, gestionDiffSamedi

        ];

        gestionDiffActiveAujourdhui = joursBooleans[currentDay];

    }

}

  

// ✅ NOUVEAUTÉS V8 : Récupération des helpers Novalix

const modeFournisseur = getState(haEntities, 'input_select.mode_fournisseur_electrique') || 'Tempo';

const novalixSeuilRechargeAprem = getState(haEntities, 'input_number.novalix_seuil_recharge_aprem') || 50;

const novalixSurconfortHC = getState(haEntities, 'input_number.novalix_surconfort_hc') || 1.0;

  

// ✅ CALCUL DES PLAGES HORAIRES NOVALIX

const heureActuelle = now.getHours();

const minuteActuelle = now.getMinutes();

const minutesTotales = heureActuelle * 60 + minuteActuelle;

  

// HC Novalix nuit : 01h00-07h00

const isHCNovalixNuit = (heureActuelle >= 1 && heureActuelle < 7);

  

// HC Novalix après-midi : 14h30-16h30

const minutesDebutHCAprem = 14 * 60 + 30; // 870 minutes

const minutesFinHCAprem = 16 * 60 + 30;   // 990 minutes

const isHCNovalixAprem = (minutesTotales >= minutesDebutHCAprem && minutesTotales < minutesFinHCAprem);

  

// --- Création du payload ---

const newPayload = {

    COULEUR_TEMPO: getState(haEntities, 'sensor.rte_tempo_couleur_actuelle'),

    COULEUR_TEMPO_PROCHAINE: getState(haEntities, 'sensor.rte_tempo_prochaine_couleur'),

    HEURE: heureActuelle,

    MINUTE: minuteActuelle,

    METEO_JOUR: meteoJour,

    MAISON_INHABITEE: maisonInhabitee,

    NIVEAU_BATTERIE_MAISON: getState(haEntities, 'sensor.sofar_battery_soc'),

    SURPLUS_PUISSANCE: surplusPuissance,

    PUISSANCE_SOLAIRE_ACTUELLE: getState(haEntities, 'sensor.sofar_solar_pv_generation'),

    PUISSANCE_RESEAU: puissanceReseau,

    TEMP_EXTERIEURE_ACTUELLE: getAttribute(haEntities, 'weather.vergeze', 'temperature'),

    PERSONNES_PRESENTES: personnesPresentes,

    PROFIL_PRESENCE: profilPresence,

    IS_YOHAN_PRESENT: isYohanPresent,

    IS_JENNY_PRESENTE: isJennyPresente,

    IS_INVITE_PRESENT: isInvitePresent,

    NIVEAU_BATTERIE_VOITURE: getState(haEntities, 'sensor.volvo_xc40_batterie') || -1,

  

    // Helpers de température

    TEMP_CONFORT_HIVER: getState(haEntities, 'input_number.temp_confort_hiver'),

    TEMP_ECO_NUIT_HIVER: getState(haEntities, 'input_number.temp_eco_nuit_hiver'),

    TEMP_CONFORT_ETE: getState(haEntities, 'input_number.temp_confort_ete'),

    TEMP_ECO_NUIT_ETE: getState(haEntities, 'input_number.temp_eco_nuit_ete'),

    TEMP_HORS_GEL: getState(haEntities, 'input_number.temp_hors_gel'),

    TEMP_ECO_JOUR_ROUGE: getState(haEntities, 'input_number.temp_eco_jour_rouge'),

  

    // Helpers d'horaires

    ACTIVATION_PRECHAUFFAGE_MATIN: getState(haEntities, 'input_boolean.activation_prechauffage_matin'),

    HEURE_DEBUT_PRECHAUFFAGE_MATIN: getState(haEntities, 'input_number.heure_debut_prechauffage_matin'),

    HEURE_FIN_PRECHAUFFAGE_MATIN: getState(haEntities, 'input_number.heure_fin_prechauffage_matin'),

    ACTIVATION_PRECHAUFFAGE_SOIR: getState(haEntities, 'input_boolean.activation_prechauffage_soir'),

    HEURE_DEBUT_PRECHAUFFAGE_SOIR: getState(haEntities, 'input_number.heure_debut_prechauffage_soir'),

    HEURE_FIN_PRECHAUFFAGE_SOIR: getState(haEntities, 'input_number.heure_fin_prechauffage_soir'),

    VENTILATION_AUTO_MI_SAISON: getState(haEntities, 'input_boolean.ventilation_auto_mi_saison'),

    HEURE_DEBUT_VENTILATION: getState(haEntities, 'input_number.heure_debut_ventilation_mi_saison'),

    HEURE_FIN_VENTILATION: getState(haEntities, 'input_number.heure_fin_ventilation_mi_saison'),

  

    // Helpers de gestion dynamique

    AJUSTEMENT_TEMP_DYNAMIQUE: getState(haEntities, 'input_number.ajustement_temp_dynamique'),

    SEUIL_MODULATION_ECO: getState(haEntities, 'input_number.seuil_modulation_eco'),

    SEUIL_BOOST_SOLAIRE: getState(haEntities, 'input_number.seuil_boost_solaire'),

    SEUIL_FROID_EXTERIEUR: getState(haEntities, 'input_number.seuil_froid_exterieur'),

    SEUIL_CHAUD_EXTERIEUR: getState(haEntities, 'input_number.seuil_chaud_exterieur'),

    GESTION_DYNAMIQUE_CONSO_ELEC: getState(haEntities, 'input_boolean.gestion_dynamique_conso_elec'),

    GESTION_DYNAMIQUE_TEMP_EXT: getState(haEntities, 'input_boolean.gestion_dynamique_temp_exterieure'),

    SEUIL_TEMP_MAX_HIVER: getState(haEntities, 'input_number.seuil_temp_max_hiver'),

    SEUIL_TEMP_MAX_ETE: getState(haEntities, 'input_number.seuil_temp_max_ete'),

    HYSTERESIS_SAISON: getState(haEntities, 'input_number.hysteresis_saison'),

  

    // Helpers de gestion auto

    GESTION_AUTO_BUREAU: getState(haEntities, 'input_boolean.gestion_auto_bureau'),

    GESTION_AUTO_SALON: getState(haEntities, 'input_boolean.gestion_auto_salon'),

    GESTION_AUTO_CH_PARENTS: getState(haEntities, 'input_boolean.gestion_auto_ch_parents'),

    GESTION_AUTO_CH_LEA: getState(haEntities, 'input_boolean.gestion_auto_ch_lea'),

    GESTION_AUTO_CH_CELIA: getState(haEntities, 'input_boolean.gestion_auto_ch_celia'),

  

    // Helpers de forçage

    DUREE_FORCAGE_MANUEL: getState(haEntities, 'input_number.duree_forcage_manuel'),

    VOITURE_CHARGE_FORCEE: getState(haEntities, 'input_boolean.voiture_charge_forcee'),

    CONSIGNE_CHARGE_VOITURE: getState(haEntities, 'input_number.voiture_consigne_charge'),

    SEUIL_SURPLUS_SOLAIRE_VOITURE: getState(haEntities, 'input_number.seuil_surplus_solaire_voiture'),

  

    // Helpers Batterie

    GESTION_AUTO_BATTERIE: getState(haEntities, 'input_boolean.gestion_automatique_batterie'),

    MODE_FORCE_BATTERIE_SELECT: getState(haEntities, 'input_select.batterie_maison_mode_force'),

    SEUIL_BATTERIE_AUTO_SOLAIRE_NUIT: getState(haEntities, 'input_number.seuil_batterie_auto_solaire_nuit'),

  

    // Helpers de gestion différenciée

    GESTION_PRESENCE_DIFFERENTIELLE: gestionPresenceDifferentielle,

    GESTION_DIFF_ACTIVE_AUJOURDHUI: gestionDiffActiveAujourdhui,

    HEURE_DEBUT_PAUSE_DEJEUNER: heureDebutPauseDejeuner,

    MINUTE_DEBUT_PAUSE_DEJEUNER: minuteDebutPauseDejeuner,

    HEURE_FIN_PAUSE_DEJEUNER: heureFinPauseDejeuner,

    MINUTE_FIN_PAUSE_DEJEUNER: minuteFinPauseDejeuner,

    HEURE_RETOUR_MODE_NORMAL: heureRetourModeNormal,

    MINUTE_RETOUR_MODE_NORMAL: minuteRetourModeNormal,

  

    // ✅ NOUVEAUX HELPERS V8 NOVALIX

    MODE_FOURNISSEUR: modeFournisseur,

    NOVALIX_SEUIL_RECHARGE_APREM: novalixSeuilRechargeAprem,

    NOVALIX_SURCONFORT_HC: novalixSurconfortHC,

    IS_HC_NOVALIX_NUIT: isHCNovalixNuit,

    IS_HC_NOVALIX_APREM: isHCNovalixAprem,

  

    // Objets complexes

    CONSIGNES_ACTUELLES: {

        BUREAU: getAttribute(haEntities, 'climate.bureau', 'temperature'),

        SALON: getAttribute(haEntities, 'climate.salon', 'temperature'),

        CHAMBRE_PARENTS: getAttribute(haEntities, 'climate.parents', 'temperature'),

        CHAMBRE_LEA: getAttribute(haEntities, 'climate.lea', 'temperature'),

        CHAMBRE_CELIA: getAttribute(haEntities, 'climate.celia', 'temperature'),

    },

    ETATS_CLIM: {

        BUREAU: getState(haEntities, 'climate.bureau'),

        SALON: getState(haEntities, 'climate.salon'),

        CHAMBRE_PARENTS: getState(haEntities, 'climate.parents'),

        CHAMBRE_LEA: getState(haEntities, 'climate.lea'),

        CHAMBRE_CELIA: getState(haEntities, 'climate.celia'),

    },

    OVERRIDE_TIMESTAMPS: {

        BUREAU: getState(haEntities, 'input_text.override_timestamp_bureau'),

        SALON: getState(haEntities, 'input_text.override_timestamp_salon'),

        CHAMBRE_PARENTS: getState(haEntities, 'input_text.override_timestamp_chambre_parents'),

        CHAMBRE_LEA: getState(haEntities, 'input_text.override_timestamp_chambre_lea'),

        CHAMBRE_CELIA: getState(haEntities, 'input_text.override_timestamp_chambre_celia'),

    },

    LAST_TEMP_SET: {

        BUREAU: getState(haEntities, 'input_number.last_temp_set_bureau'),

        SALON: getState(haEntities, 'input_number.last_temp_set_salon'),

        CHAMBRE_PARENTS: getState(haEntities, 'input_number.last_temp_set_chambre_parents'),

        CHAMBRE_LEA: getState(haEntities, 'input_number.last_temp_set_chambre_lea'),

        CHAMBRE_CELIA: getState(haEntities, 'input_number.last_temp_set_chambre_celia'),

    },

    RAPPORT_DEBUG_MODE: getState(haEntities, 'input_boolean.rapports_maison'),

    HEURE_RAPPORT_QUOTIDIEN: getState(haEntities, 'input_number.heure_rapport_quotidien'),

    MINUTE_RAPPORT_QUOTIDIEN: getState(haEntities, 'input_number.minute_rapport_quotidien')

};

  

// Logique jour rouge (uniquement si mode Tempo)

newPayload.IS_NIGHT_BEFORE_RED_DAY = false;

if (modeFournisseur === 'Tempo') {

    newPayload.IS_NIGHT_BEFORE_RED_DAY = (

        newPayload.HEURE >= 22 &&

        newPayload.COULEUR_TEMPO_PROCHAINE === 'Rouge' &&

        newPayload.COULEUR_TEMPO !== 'Rouge'

    );

}

  

// Intégration des données météo

if (forecastData && forecastData.forecast && forecastData.forecast.length > 0) {

    const bonneMeteoStatesForecast = ['sunny', 'partlycloudy', 'clear-night'];

    newPayload.METEO_DEMAIN = bonneMeteoStatesForecast.includes(forecastData.forecast[0].condition) ? 'BONNE' : 'MAUVAISE';

    newPayload.TEMP_MAX_PREVUE = forecastData.forecast[0].temperature;

} else {

    newPayload.METEO_DEMAIN = 'INCONNUE';

    newPayload.TEMP_MAX_PREVUE = null;

}

  

// --- LOGIQUE DE DÉTECTION SAISONNIÈRE ---

let calculatedSeason = flow.get('calculated_season');

const tempExt = newPayload.TEMP_EXTERIEURE_ACTUELLE;

const seuilHiver = newPayload.SEUIL_TEMP_MAX_HIVER;

const seuilEte = newPayload.SEUIL_TEMP_MAX_ETE;

const hysteresis = newPayload.HYSTERESIS_SAISON;

  

const moisHiver = [11, 0, 1];

const moisMiSaisonHiver = [2, 3, 10];

const moisMiSaisonEte = [4, 8, 9];

const moisEte = [5, 6, 7];

  

let tempBasedSeason;

  

if (calculatedSeason === undefined || calculatedSeason === null) {

    if (tempExt <= seuilHiver) {

        tempBasedSeason = 'HIVER';

    } else if (tempExt >= seuilEte) {

        tempBasedSeason = 'ÉTÉ';

    } else if (tempExt < (seuilHiver + seuilEte) / 2) {

        tempBasedSeason = 'MI-SAISON-HIVER';

    } else {

        tempBasedSeason = 'MI-SAISON-ETE';

    }

} else {

    if (calculatedSeason === 'HIVER' && tempExt > seuilHiver + hysteresis) {

        tempBasedSeason = 'MI-SAISON-HIVER';

    } else if (calculatedSeason === 'MI-SAISON-HIVER' && tempExt < seuilHiver - hysteresis) {

        tempBasedSeason = 'HIVER';

    } else if (calculatedSeason === 'MI-SAISON-HIVER' && tempExt > seuilEte - hysteresis) {

        tempBasedSeason = 'MI-SAISON-ETE';

    } else if (calculatedSeason === 'MI-SAISON-ETE' && tempExt < seuilHiver + hysteresis) {

        tempBasedSeason = 'MI-SAISON-HIVER';

    } else if (calculatedSeason === 'MI-SAISON-ETE' && tempExt > seuilEte - hysteresis) {

        tempBasedSeason = 'ÉTÉ';

    } else if (calculatedSeason === 'ÉTÉ' && tempExt < seuilEte - hysteresis) {

        tempBasedSeason = 'MI-SAISON-ETE';

    } else {

        tempBasedSeason = calculatedSeason;

    }

}

  

let monthBasedSeason;

if (moisHiver.includes(currentMonth)) {

    monthBasedSeason = 'HIVER';

} else if (moisMiSaisonHiver.includes(currentMonth)) {

    monthBasedSeason = 'MI-SAISON-HIVER';

} else if (moisMiSaisonEte.includes(currentMonth)) {

    monthBasedSeason = 'MI-SAISON-ETE';

} else if (moisEte.includes(currentMonth)) {

    monthBasedSeason = 'ÉTÉ';

} else {

    monthBasedSeason = 'INCONNU';

}

  

const saisonPrioritaire = ['HIVER', 'MI-SAISON-HIVER', 'ÉTÉ', 'MI-SAISON-ETE'].indexOf(tempBasedSeason) <= ['HIVER', 'MI-SAISON-HIVER', 'ÉTÉ', 'MI-SAISON-ETE'].indexOf(monthBasedSeason) ? tempBasedSeason : monthBasedSeason;

  

if (saisonPrioritaire !== calculatedSeason) {

    flow.set('calculated_season', saisonPrioritaire);

}

  

newPayload.MODE_SAISONNIER = saisonPrioritaire;

  

// ============================================================

// ✅ V8 : GESTION MODE SÉCURITÉ (capteurs critiques manquants)

// ============================================================

const capteursCritiques = [

    { nom: 'NIVEAU_BATTERIE_MAISON', valeur: newPayload.NIVEAU_BATTERIE_MAISON },

    { nom: 'PUISSANCE_RESEAU', valeur: newPayload.PUISSANCE_RESEAU },

    { nom: 'PUISSANCE_SOLAIRE_ACTUELLE', valeur: newPayload.PUISSANCE_SOLAIRE_ACTUELLE },

    { nom: 'TEMP_EXTERIEURE_ACTUELLE', valeur: newPayload.TEMP_EXTERIEURE_ACTUELLE }

];

  

const capteursManquants = capteursCritiques.filter(c => c.valeur === null || c.valeur === undefined);

  

if (capteursManquants.length > 0) {

    newPayload.MODE_SECURITE = true;

    newPayload.CAPTEURS_MANQUANTS = capteursManquants.map(c => c.nom);

    node.warn(`⚠️ MODE SÉCURITÉ ACTIVÉ - Capteurs critiques manquants : ${capteursManquants.map(c => c.nom).join(', ')}`);

} else {

    newPayload.MODE_SECURITE = false;

    newPayload.CAPTEURS_MANQUANTS = [];

}

  

msg.payload = newPayload;

return msg;

**CODE DU NŒUD : LOGIQUE BATTERIE (Fonction)**

// ============================================================

// NŒUD : LOGIQUE BATTERIE - VERSION V8 NOVALIX

// Type: Function

// ============================================================

// Ce nœud gère la stratégie de charge/décharge de la batterie

// domestique selon le fournisseur d'électricité sélectionné.

//

// ✅ NOUVEAUTÉS V8 :

// - Support double fournisseur (Tempo / Novalix)

// - Stratégie Novalix avec 2 plages HC (nuit + après-midi)

// - Recharge complémentaire après-midi si batterie < seuil

// - Suppression logique ECONOMIE_ECONOMIE_ON/OFF en mode Novalix

// ============================================================

  

try {  

    const payload = msg.payload;  

  

    const {

        HEURE, MINUTE, METEO_DEMAIN, MODE_SAISONNIER,

        IS_NIGHT_BEFORE_RED_DAY, GESTION_AUTO_BATTERIE, MODE_FORCE_BATTERIE_SELECT,

        NIVEAU_BATTERIE_MAISON, SEUIL_BATTERIE_AUTO_SOLAIRE_NUIT,

        MODE_FOURNISSEUR, IS_HC_NOVALIX_NUIT, IS_HC_NOVALIX_APREM,

        NOVALIX_SEUIL_RECHARGE_APREM, COULEUR_TEMPO

    } = payload;

  

    msg.commands = msg.commands || [];

    payload.rapportActionBatterie = "AUTO"; // Valeur par défaut

  

    // ============================================================

    // ✅ V8 : MODE SÉCURITÉ (capteurs critiques manquants)

    // ============================================================

    if (payload.MODE_SECURITE) {

        // En mode sécurité : batterie en AUTO uniquement, pas de charge/décharge forcée

        msg.commands.push('PACK_BATTERIE_AUTO');

        payload.rapportActionBatterie = "SECURITE_AUTO";

        node.warn(`⚠️ Batterie en mode sécurité AUTO - Capteurs manquants : ${payload.CAPTEURS_MANQUANTS.join(', ')}`);

        return msg;

    }

  

    // --- LOGIQUE AUTOMATIQUE ---

    if (GESTION_AUTO_BATTERIE) {

        // ========================================================

        // MODE TEMPO (ANCIEN COMPORTEMENT)

        // ========================================================

        if (MODE_FOURNISSEUR === 'Tempo') {

            const isNightTime = HEURE >= 22 || HEURE < 6;

  

            // --- GESTION NOCTURNE (22h-06h) ---

            if (isNightTime) {

                // PRIORITÉ 1 : Anticiper un jour Rouge

                if (IS_NIGHT_BEFORE_RED_DAY) {

                    msg.commands.push('PACK_BATTERIE_CHARGE');

                    payload.rapportActionBatterie = "CHARGE_ANTICIPEE_ROUGE";

                    return msg;

                }

  

                // PRIORITÉ 2 : Stratégie "Solaire" (Été / Mi-saison été)

                if (MODE_SAISONNIER === 'ÉTÉ' || MODE_SAISONNIER === 'MI-SAISON-ETE') {

                    if (METEO_DEMAIN === 'BONNE') {

                        if (NIVEAU_BATTERIE_MAISON >= SEUIL_BATTERIE_AUTO_SOLAIRE_NUIT) {

                            msg.commands.push('PACK_BATTERIE_AUTO');

                            payload.rapportActionBatterie = "AUTO_POUR_SOLAIRE";

                        } else {

                            msg.commands.push('PACK_BATTERIE_CHARGE');

                            payload.rapportActionBatterie = "CHARGE_MINIMALE_SOLAIRE";

                        }

                        return msg;

                    } else if (METEO_DEMAIN === 'MAUVAISE') {

                        msg.commands.push('PACK_BATTERIE_CHARGE');

                        payload.rapportActionBatterie = "CHARGE_MAUVAIS_TEMPS";

                        return msg;

                    } else {

                        msg.commands.push('PACK_BATTERIE_CHARGE');

                        payload.rapportActionBatterie = "CHARGE_METEO_INCONNUE";

                        return msg;

                    }

                }

                // PRIORITÉ 3 : Stratégie "Hiver"

                msg.commands.push('PACK_BATTERIE_CHARGE');

                payload.rapportActionBatterie = "CHARGE_HIVER";

                return msg;

            }

  

            // --- GESTION JOURNÉE (06h-22h) ---

            else {

                if (COULEUR_TEMPO === 'Rouge') {

                    payload.rapportActionBatterie = "DECHARGE_JOUR_ROUGE";

                }

                msg.commands.push('PACK_BATTERIE_AUTO');

            }

  

            // Logique ECONOMIE_ECONOMIE_ON/OFF pour les jours rouges Tempo

            if (COULEUR_TEMPO === 'Rouge') {

                if (HEURE === 6 && MINUTE < 5) { msg.commands.push('ECONOMIE_ECONOMIE_ON'); }

                if (HEURE === 22 && MINUTE < 5) { msg.commands.push('ECONOMIE_ECONOMIE_OFF'); }

            }

            return msg;

        }

        // ========================================================

        // MODE NOVALIX (NOUVELLE STRATÉGIE SIMPLIFIÉE)

        // ========================================================

        else if (MODE_FOURNISSEUR === 'Novalix') {

            // --- GESTION HC NUIT (01h-07h) ---

            // Stratégie simple : TOUJOURS charger pour avoir 100% le matin

            // Pas besoin d'intelligence météo car on a la plage après-midi en complément

            if (IS_HC_NOVALIX_NUIT) {

                msg.commands.push('PACK_BATTERIE_CHARGE');

                payload.rapportActionBatterie = "NOVALIX_CHARGE_NUIT";

                return msg;

            }

            // --- GESTION HC APRÈS-MIDI (14h30-16h30) ✨ NOUVEAU ---

            else if (IS_HC_NOVALIX_APREM) {

                // Recharge complémentaire si batterie sous le seuil

                if (NIVEAU_BATTERIE_MAISON < NOVALIX_SEUIL_RECHARGE_APREM) {

                    msg.commands.push('PACK_BATTERIE_CHARGE');

                    payload.rapportActionBatterie = "NOVALIX_RECHARGE_APREM";

                } else {

                    // Batterie suffisante, on laisse en auto pour utiliser le solaire

                    msg.commands.push('PACK_BATTERIE_AUTO');

                    payload.rapportActionBatterie = "NOVALIX_AUTO_APREM_OK";

                }

                return msg;

            }

            // --- GESTION HEURES PLEINES ---

            else {

                // En dehors des HC, toujours en mode AUTO

                msg.commands.push('PACK_BATTERIE_AUTO');

                payload.rapportActionBatterie = "NOVALIX_AUTO_HP";

                return msg;

            }

        }

    }

    // --- LOGIQUE MANUELLE (inchangée) ---

    else {

        let forcedModeCommand;

        switch (MODE_FORCE_BATTERIE_SELECT) {

            case 'Charge':

                forcedModeCommand = 'PACK_BATTERIE_CHARGE';

                payload.rapportActionBatterie = "FORCE_CHARGE";

                break;

            case 'Equalization':

                forcedModeCommand = 'PACK_BATTERIE_EQUALIZATION';

                payload.rapportActionBatterie = "FORCE_EQUALIZATION";

                break;

            case 'Discharge':

                forcedModeCommand = 'PACK_BATTERIE_DISCHARGE';

                payload.rapportActionBatterie = "FORCE_DECHARGE";

                break;

            case 'Standby':

                forcedModeCommand = 'PACK_BATTERIE_STANDBY';

                payload.rapportActionBatterie = "FORCE_STANDBY";

                break;

            case 'Auto':

                forcedModeCommand = 'PACK_BATTERIE_AUTO';

                payload.rapportActionBatterie = "AUTO_ONDULEUR_INTERNE";

                break;

            default:

                forcedModeCommand = 'PACK_BATTERIE_AUTO';

                payload.rapportActionBatterie = "AUTO_ONDULEUR_INTERNE";

                break;

        }

        msg.commands.push(forcedModeCommand);

        return msg;

    }

  

} catch (e) {  

    node.error(e.stack, msg);  

    return null;  

}

CODE DU NŒUD : LOGIQUE THERMOSTATS (Fonction)

// ============================================================

// NŒUD : LOGIQUE THERMOSTATS - VERSION V8 NOVALIX

// Type: Function

// ============================================================

// Ce nœud gère la température de toutes les pièces selon :

// - Saison, présence, heure

// - Gestion dynamique (solaire, consommation, température extérieure)

// - Préchauffages programmables

// - Gestion différenciée par personne

//

// ✅ NOUVEAUTÉS V8 :

// - Sur-confort thermique pendant HC Novalix

// - Suppression des restrictions jour Rouge pour Novalix

// - Logique jour Rouge conservée uniquement pour Tempo

// ============================================================

  

try {

    const payload = msg.payload;

    const {

        HEURE, MINUTE, METEO_JOUR, MAISON_INHABITEE, PROFIL_PRESENCE,

        NIVEAU_BATTERIE_MAISON, CONSIGNES_ACTUELLES, ETATS_CLIM, DUREE_FORCAGE_MANUEL,

        TEMP_EXTERIEURE_ACTUELLE, SURPLUS_PUISSANCE, TEMP_CONFORT_HIVER, TEMP_ECO_NUIT_HIVER,

        TEMP_ECO_NUIT_ETE, TEMP_CONFORT_ETE, TEMP_HORS_GEL, TEMP_ECO_JOUR_ROUGE,

        ACTIVATION_PRECHAUFFAGE_MATIN, HEURE_DEBUT_PRECHAUFFAGE_MATIN, HEURE_FIN_PRECHAUFFAGE_MATIN,

        ACTIVATION_PRECHAUFFAGE_SOIR, HEURE_DEBUT_PRECHAUFFAGE_SOIR, HEURE_FIN_PRECHAUFFAGE_SOIR,

        GESTION_AUTO_BUREAU, GESTION_AUTO_SALON, GESTION_AUTO_CH_PARENTS,

        GESTION_AUTO_CH_LEA, GESTION_AUTO_CH_CELIA,

        VENTILATION_AUTO_MI_SAISON, HEURE_DEBUT_VENTILATION, HEURE_FIN_VENTILATION,

        AJUSTEMENT_TEMP_DYNAMIQUE, SEUIL_MODULATION_ECO, SEUIL_BOOST_SOLAIRE,

        SEUIL_FROID_EXTERIEUR, SEUIL_CHAUD_EXTERIEUR, IS_NIGHT_BEFORE_RED_DAY,

        LAST_TEMP_SET, OVERRIDE_TIMESTAMPS, PERSONNES_PRESENTES,

        PUISSANCE_SOLAIRE_ACTUELLE, PUISSANCE_RESEAU,

        GESTION_DYNAMIQUE_CONSO_ELEC, GESTION_DYNAMIQUE_TEMP_EXT,

        MODE_SAISONNIER = 'INCONNU',

        GESTION_PRESENCE_DIFFERENTIELLE = false,

        GESTION_DIFF_ACTIVE_AUJOURDHUI = false,

        HEURE_DEBUT_PAUSE_DEJEUNER = 12,

        MINUTE_DEBUT_PAUSE_DEJEUNER = 0,

        HEURE_FIN_PAUSE_DEJEUNER = 13,

        MINUTE_FIN_PAUSE_DEJEUNER = 30,

        HEURE_RETOUR_MODE_NORMAL = 17,

        MINUTE_RETOUR_MODE_NORMAL = 30,

        // ✅ NOUVEAUX PARAMÈTRES V8 NOVALIX

        MODE_FOURNISSEUR = 'Tempo',

        IS_HC_NOVALIX_NUIT = false,

        IS_HC_NOVALIX_APREM = false,

        NOVALIX_SURCONFORT_HC = 1.0,

        COULEUR_TEMPO = null

    } = payload;

  

    msg.commands = msg.commands || [];

  

    const PIECES = ['BUREAU', 'SALON', 'CHAMBRE_PARENTS', 'CHAMBRE_LEA', 'CHAMBRE_CELIA'];

    const CHAMBRES_IDS = ['CHAMBRE_PARENTS', 'CHAMBRE_LEA', 'CHAMBRE_CELIA'];

    const PIECES_VIE_IDS = ['BUREAU', 'SALON'];

  

    payload.rapportAjustementTemp = "Aucun";

    payload.rapportModeTemp = "Aucun";

  

    // ============================================================

    // ✅ V8 : MODE SÉCURITÉ (capteurs critiques manquants)

    // ============================================================

    if (payload.MODE_SECURITE) {

        // En mode sécurité : températures éco partout, pas d'ajustements dynamiques

        node.warn(`⚠️ Thermostats en mode sécurité ECO - Capteurs manquants : ${payload.CAPTEURS_MANQUANTS.join(', ')}`);

        const tempSecurite = (MODE_SAISONNIER === 'HIVER' || MODE_SAISONNIER === 'MI-SAISON-HIVER')

            ? TEMP_ECO_NUIT_HIVER

            : TEMP_ECO_NUIT_ETE;

        PIECES.forEach(piece => setClim(piece, 'ON', tempSecurite));

        payload.rapportModeTemp = 'Securite_ECO';

        payload.rapportChauffage = `Mode sécurité activé : températures en mode économique (${tempSecurite}°C) en attente de rétablissement des capteurs.`;

        return msg;

    }

  

    // ============================================================

    // FONCTION : CALCUL TEMPÉRATURE CONFORT DYNAMIQUE

    // ✅ V8 : Ajout du sur-confort HC Novalix

    // ============================================================

    function getTempConfortDynamique(baseTemp, saison) {

        let ajustementTotal = 0;

        const ajustementReasons = [];

        const isDaytime = (HEURE >= 7 && HEURE < 21);

        const isConfortHours = (HEURE >= 5 && HEURE < 23);

  

        // ✅ NOUVEAU V8 : Sur-confort pendant HC Novalix

        if (MODE_FOURNISSEUR === 'Novalix' && NOVALIX_SURCONFORT_HC > 0) {

            if (IS_HC_NOVALIX_NUIT || IS_HC_NOVALIX_APREM) {

                ajustementReasons.push(`Surconfort_HC_Novalix`);

                ajustementTotal += NOVALIX_SURCONFORT_HC;

            }

        }

  

        if (AJUSTEMENT_TEMP_DYNAMIQUE > 0) {

            if (GESTION_DYNAMIQUE_CONSO_ELEC) {

                const conditionBoostSolaire = isDaytime && SURPLUS_PUISSANCE > SEUIL_BOOST_SOLAIRE;

                if (conditionBoostSolaire) {

                    ajustementReasons.push("Boost_Solaire");

                    if (saison === 'HIVER' || saison === 'MI-SAISON-HIVER') ajustementTotal += AJUSTEMENT_TEMP_DYNAMIQUE;

                    else if (saison === 'ÉTÉ' || saison === 'MI-SAISON-ETE') ajustementTotal -= AJUSTEMENT_TEMP_DYNAMIQUE;

                }

  

                const conditionEcoModulation = PUISSANCE_RESEAU > 0 && PUISSANCE_RESEAU > SEUIL_MODULATION_ECO;

                if (conditionEcoModulation) {

                    ajustementReasons.push("Eco_Modulation");

                    if (saison === 'HIVER' || saison === 'MI-SAISON-HIVER') ajustementTotal -= AJUSTEMENT_TEMP_DYNAMIQUE;

                    else if (saison === 'ÉTÉ' || saison === 'MI-SAISON-ETE') ajustementTotal += AJUSTEMENT_TEMP_DYNAMIQUE;

                }

            }

  

            if (GESTION_DYNAMIQUE_TEMP_EXT && isConfortHours) {

                const conditionFroidExterieur = (saison === 'HIVER' || saison === 'MI-SAISON-HIVER') && TEMP_EXTERIEURE_ACTUELLE < SEUIL_FROID_EXTERIEUR;

                if (conditionFroidExterieur) {

                    ajustementReasons.push("Froid_Extérieur");

                    ajustementTotal += AJUSTEMENT_TEMP_DYNAMIQUE;

                }

  

                const conditionChaleurExterieure = (saison === 'ÉTÉ' || saison === 'MI-SAISON-ETE') && TEMP_EXTERIEURE_ACTUELLE > SEUIL_CHAUD_EXTERIEUR;

                if (conditionChaleurExterieure) {

                    ajustementReasons.push("Chaleur_Extérieure");

                    ajustementTotal -= AJUSTEMENT_TEMP_DYNAMIQUE;

                }

            }

        }

  

        if (ajustementReasons.length > 0) {

            payload.rapportAjustementTemp = ajustementReasons.join('+');

        }

  

        return baseTemp + ajustementTotal;

    }

  

    // ============================================================

    // FONCTION : COMMANDE CLIMATISATION

    // ============================================================

    function setClim(piece, state, temp = null) {

        const activationMap = {

            'BUREAU': GESTION_AUTO_BUREAU,

            'SALON': GESTION_AUTO_SALON,

            'CHAMBRE_PARENTS': GESTION_AUTO_CH_PARENTS,

            'CHAMBRE_LEA': GESTION_AUTO_CH_LEA,

            'CHAMBRE_CELIA': GESTION_AUTO_CH_CELIA

        };

        if (!activationMap[piece]) return;

  

        const tempIdeale = LAST_TEMP_SET[piece] || temp;

        const etatActuel = ETATS_CLIM[piece];

        const overrideTimestamp = OVERRIDE_TIMESTAMPS[piece];

  

        let overrideEntitySuffix;

        switch (piece) {

            case 'BUREAU': overrideEntitySuffix = 'override_timestamp_bureau'; break;

            case 'SALON': overrideEntitySuffix = 'override_timestamp_salon'; break;

            case 'CHAMBRE_PARENTS': overrideEntitySuffix = 'override_timestamp_chambre_parents'; break;

            case 'CHAMBRE_LEA': overrideEntitySuffix = 'override_timestamp_chambre_lea'; break;

            case 'CHAMBRE_CELIA': overrideEntitySuffix = 'override_timestamp_chambre_celia'; break;

            default: return;

        }

  

        const manualOverrideActive = (etatActuel !== 'off' && temp !== null && CONSIGNES_ACTUELLES[piece] !== tempIdeale);

  

        if (manualOverrideActive) {

            const now = Date.now();

            let timestamp = overrideTimestamp ? parseInt(overrideTimestamp) : null;

            if (!timestamp) {

                timestamp = now;

                msg.commands.push(`SET_INPUT_TEXT_${overrideEntitySuffix}_${timestamp}`);

            }

            const elapsedHours = (now - timestamp) / 3600000;

            if (elapsedHours < DUREE_FORCAGE_MANUEL) return;

            msg.commands.push(`SET_INPUT_TEXT_${overrideEntitySuffix}_`);

        } else {

            if (overrideTimestamp) msg.commands.push(`SET_INPUT_TEXT_${overrideEntitySuffix}_`);

        }

  

        if (state === 'ON') {

            msg.commands.push(`THERMOSTAT_${piece}_ON`);

            if (temp !== null) {

                msg.commands.push(`${piece}_TEMP_${temp}`);

                msg.commands.push(`SET_INPUT_NUMBER_last_temp_set_${piece.toLowerCase()}_${temp}`);

            }

        } else if (state === 'OFF') {

            msg.commands.push(`THERMOSTAT_${piece}_OFF`);

            if (temp !== null) {

                msg.commands.push(`${piece}_TEMP_${temp}`);

                msg.commands.push(`SET_INPUT_NUMBER_last_temp_set_${piece.toLowerCase()}_${temp}`);

            }

        }

    }

  

    // ============================================================

    // SÉCURITÉ BATTERIE 16h (UNIQUEMENT TEMPO)

    // ✅ V8 : Désactivé en mode Novalix

    // ============================================================

    if (MODE_FOURNISSEUR === 'Tempo' && COULEUR_TEMPO === 'Rouge' && HEURE >= 16 && HEURE < 17) {

        const SEUIL_SECURITE_BATTERIE_16H = 50;

        if (NIVEAU_BATTERIE_MAISON < SEUIL_SECURITE_BATTERIE_16H) {

            PIECES.forEach(piece => setClim(piece, 'OFF', TEMP_HORS_GEL));

            payload.rapportModeTemp = 'Securite_Batterie_16h';

            payload.rapportChauffage = `Action de sécurité à 16h : batterie (${NIVEAU_BATTERIE_MAISON}%) sous le seuil de ${SEUIL_SECURITE_BATTERIE_16H}%. Chauffage coupé.`;

            return msg;

        }

    }

  

    // ============================================================

    // FONCTION PRINCIPALE DE GESTION

    // ============================================================

    function gestionCommune() {

        // Gestion piscine

        if (MODE_SAISONNIER === 'ÉTÉ' || MODE_SAISONNIER === 'MI-SAISON-ETE') {

            msg.commands.push('MODE_AUTO_PISCINE_ON');

        } else {

            msg.commands.push('MODE_AUTO_PISCINE_OFF');

        }

  

        // ✅ V8 : Logique jour Rouge uniquement en mode Tempo

        let isDaytimeRedRestriction = false;

        let isRedDaySunnyException = false;

        if (MODE_FOURNISSEUR === 'Tempo' && COULEUR_TEMPO === 'Rouge') {

            isDaytimeRedRestriction = (HEURE >= 6 && HEURE < 22);

            isRedDaySunnyException = isDaytimeRedRestriction && METEO_JOUR === 'BONNE' && !MAISON_INHABITEE;

        }

  

        // CAS 1 : MAISON INHABITÉE OU JOUR ROUGE RESTRICTIF (TEMPO UNIQUEMENT)

        if (MAISON_INHABITEE || (isDaytimeRedRestriction && !isRedDaySunnyException)) {

            PIECES.forEach(piece => setClim(piece, 'OFF', TEMP_HORS_GEL));

            payload.rapportModeTemp = 'Hors_Gel';

            msg.commands.push('VMC_MODE_VACANCES');

        }

        // CAS 2 : JOUR ROUGE ENSOLEILLÉ (TEMPO UNIQUEMENT)

        else if (isRedDaySunnyException) {

            if (PUISSANCE_SOLAIRE_ACTUELLE > 2000) {

                let tempConfortRougeSolaire = getTempConfortDynamique(TEMP_ECO_JOUR_ROUGE, 'HIVER');

                PIECES_VIE_IDS.forEach(piece => setClim(piece, 'ON', tempConfortRougeSolaire));

                CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', TEMP_ECO_NUIT_HIVER));

                payload.rapportModeTemp = 'Eco_Solaire_Rouge';

                payload.rapportChauffage = `Jour Rouge ensoleillé : Chauffage des pièces de vie à ${tempConfortRougeSolaire.toFixed(1)}°C grâce au solaire (>2000W).`;

            } else {

                PIECES.forEach(piece => setClim(piece, 'OFF', TEMP_HORS_GEL));

                payload.rapportModeTemp = 'Attente_Solaire_Rouge';

                payload.rapportChauffage = `Jour Rouge ensoleillé : Chauffage en hors-gel en attente de production solaire suffisante (>2000W).`;

            }

            msg.commands.push('VMC_MODE_NORMAL');

        }

        // CAS 3 : FONCTIONNEMENT NORMAL

        else {

            msg.commands.push('VMC_MODE_NORMAL');

  

            // MODE HIVER / MI-SAISON-HIVER

            if (MODE_SAISONNIER === 'HIVER' || MODE_SAISONNIER === 'MI-SAISON-HIVER') {

                let tempConfortHiverAjustee = getTempConfortDynamique(TEMP_CONFORT_HIVER, 'HIVER');

                payload.TEMP_CONFORT_HIVER_AJUSTEE = tempConfortHiverAjustee;

                PIECES.forEach(piece => msg.commands.push(`${piece}_MODE_CHAUFFAGE`));

  

                let isPrechauffageMatin = false;

                let isPrechauffageSoir = false;

  

                // ✅ V8 : Préchauffage anticipé jour Rouge uniquement en mode Tempo

                if (MODE_FOURNISSEUR === 'Tempo' && IS_NIGHT_BEFORE_RED_DAY) {

                    if (HEURE >= 22 && HEURE < 23) {

                        isPrechauffageSoir = true;

                        payload.rapportContexte = `Préchauffage anticipé pour le jour rouge de demain.`;

                    }

                } else {

                    if (ACTIVATION_PRECHAUFFAGE_MATIN && HEURE >= HEURE_DEBUT_PRECHAUFFAGE_MATIN && HEURE < HEURE_FIN_PRECHAUFFAGE_MATIN) {

                        isPrechauffageMatin = true;

                    }

                    if (ACTIVATION_PRECHAUFFAGE_SOIR && HEURE >= HEURE_DEBUT_PRECHAUFFAGE_SOIR && HEURE < HEURE_FIN_PRECHAUFFAGE_SOIR) {

                        isPrechauffageSoir = true;

                    }

                }

  

                const isDeepNight = (HEURE >= 23 || HEURE < 5);

  

                // NUIT PROFONDE (23h-5h)

                if (isDeepNight) {

                    CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', TEMP_ECO_NUIT_HIVER));

                    PIECES_VIE_IDS.forEach(piece => setClim(piece, 'OFF', TEMP_HORS_GEL));

                    payload.rapportModeTemp = 'Nuit_Eco';

                }

                // JOURNÉE / SOIRÉE (5h-23h)

                else {

                    if (GESTION_PRESENCE_DIFFERENTIELLE && GESTION_DIFF_ACTIVE_AUJOURDHUI) {

                        const minutesActuelles = HEURE * 60 + MINUTE;

                        const minutesDebutPause = HEURE_DEBUT_PAUSE_DEJEUNER * 60 + MINUTE_DEBUT_PAUSE_DEJEUNER;

                        const minutesFinPause = HEURE_FIN_PAUSE_DEJEUNER * 60 + MINUTE_FIN_PAUSE_DEJEUNER;

                        const minutesRetourNormal = HEURE_RETOUR_MODE_NORMAL * 60 + MINUTE_RETOUR_MODE_NORMAL;

  

                        const isPauseDejeuner = (minutesActuelles >= minutesDebutPause && minutesActuelles < minutesFinPause);

                        const isAvantRetourNormal = (minutesActuelles < minutesRetourNormal);

  

                        switch (PROFIL_PRESENCE) {

                            case 'YOHAN_SEUL':

                                setClim('BUREAU', 'ON', tempConfortHiverAjustee);

                                if (isPauseDejeuner) {

                                    setClim('SALON', 'ON', tempConfortHiverAjustee);

                                } else if (isAvantRetourNormal) {

                                    setClim('SALON', 'ON', TEMP_ECO_NUIT_HIVER);

                                } else {

                                    setClim('SALON', 'ON', tempConfortHiverAjustee);

                                }

  

                                if (isPrechauffageMatin || isPrechauffageSoir) {

                                    CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', tempConfortHiverAjustee));

                                } else {

                                    CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', TEMP_ECO_NUIT_HIVER));

                                }

  

                                if (isPauseDejeuner) {

                                    payload.rapportModeTemp = 'Yohan_Seul_Pause_Dejeuner';

                                } else if (isAvantRetourNormal) {

                                    payload.rapportModeTemp = 'Yohan_Seul_Bureau';

                                } else {

                                    payload.rapportModeTemp = 'Yohan_Seul_Normal';

                                }

                                break;

  

                            case 'JENNY_SEULE':

                            case 'INVITE_SEUL':

                                setClim('SALON', 'ON', tempConfortHiverAjustee);

  

                                if (isPauseDejeuner) {

                                    setClim('BUREAU', 'ON', tempConfortHiverAjustee);

                                } else if (isAvantRetourNormal) {

                                    setClim('BUREAU', 'ON', TEMP_ECO_NUIT_HIVER);

                                } else {

                                    setClim('BUREAU', 'ON', tempConfortHiverAjustee);

                                }

  

                                if (isPrechauffageMatin || isPrechauffageSoir) {

                                    CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', tempConfortHiverAjustee));

                                } else {

                                    CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', TEMP_ECO_NUIT_HIVER));

                                }

  

                                if (isPauseDejeuner) {

                                    payload.rapportModeTemp = PROFIL_PRESENCE === 'JENNY_SEULE' ? 'Jenny_Seule_Pause_Dejeuner' : 'Invite_Seul_Pause_Dejeuner';

                                } else if (isAvantRetourNormal) {

                                    payload.rapportModeTemp = PROFIL_PRESENCE === 'JENNY_SEULE' ? 'Jenny_Seule_Salon' : 'Invite_Seul_Salon';

                                } else {

                                    payload.rapportModeTemp = PROFIL_PRESENCE === 'JENNY_SEULE' ? 'Jenny_Seule_Normal' : 'Invite_Seul_Normal';

                                }

                                break;

  

                            case 'COUPLE':

                            case 'MIXTE':

                            default:

                                PIECES_VIE_IDS.forEach(piece => setClim(piece, 'ON', tempConfortHiverAjustee));

                                payload.rapportModeTemp = 'Hiver_Confort';

  

                                if (isPrechauffageMatin || isPrechauffageSoir) {

                                    CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', tempConfortHiverAjustee));

                                } else {

                                    CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', TEMP_ECO_NUIT_HIVER));

                                }

                                break;

                        }

                    } else {

                        PIECES_VIE_IDS.forEach(piece => setClim(piece, 'ON', tempConfortHiverAjustee));

                        payload.rapportModeTemp = 'Hiver_Confort';

  

                        if (isPrechauffageMatin || isPrechauffageSoir) {

                            CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', tempConfortHiverAjustee));

                        } else {

                            CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', TEMP_ECO_NUIT_HIVER));

                        }

                    }

                }

            }

            // MODE ÉTÉ / MI-SAISON-ÉTÉ

            else if (MODE_SAISONNIER === 'ÉTÉ' || MODE_SAISONNIER === 'MI-SAISON-ETE') {

                let tempConfortEteAjustee = getTempConfortDynamique(TEMP_CONFORT_ETE, 'ÉTÉ');

                payload.TEMP_CONFORT_ETE_AJUSTEE = tempConfortEteAjustee;

                PIECES.forEach(piece => msg.commands.push(`${piece}_MODE_CLIMATISATION`));

  

                const isDeepNight = (HEURE >= 23 || HEURE < 5);

  

                if (isDeepNight) {

                    CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', TEMP_ECO_NUIT_ETE));

                    PIECES_VIE_IDS.forEach(piece => setClim(piece, 'OFF', TEMP_HORS_GEL));

                    payload.rapportModeTemp = 'Nuit_Eco_Ete';

                } else {

                    PIECES.forEach(piece => setClim(piece, 'ON', tempConfortEteAjustee));

                    payload.rapportModeTemp = 'Ete_Confort';

                }

            }

            // MODE INCONNU

            else {

                payload.rapportModeTemp = 'Off';

                PIECES.forEach(piece => setClim(piece, 'OFF', TEMP_HORS_GEL));

            }

        }

    }

  

    gestionCommune();

    return msg;

  

} catch (e) {

    node.error(e.stack, msg);

    return null;

}

CODE DU NŒUD : LOGIQUE VÉHICULE (Fonction)

// ============================================================

// NŒUD : LOGIQUE VÉHICULE - VERSION V8 NOVALIX

// Type: Function

// ============================================================

// Ce nœud gère la charge de la voiture électrique selon :

// - Le fournisseur d'électricité (Tempo / Novalix)

// - Les plages horaires creuses

// - Le surplus solaire

// - Le mode forcé manuel

//

// ✅ NOUVEAUTÉS V8 :

// - Adaptation aux plages HC Novalix (01h-07h + 14h30-16h30)

// - Logique Tempo conservée pour compatibilité

// ============================================================

  

try {

    const payload = msg.payload;

    const {

        HEURE, NIVEAU_BATTERIE_VOITURE,

        CONSIGNE_CHARGE_VOITURE, SURPLUS_PUISSANCE, SEUIL_SURPLUS_SOLAIRE_VOITURE,

        MAISON_INHABITEE, VOITURE_CHARGE_FORCEE,

        // ✅ NOUVEAUX PARAMÈTRES V8

        MODE_FOURNISSEUR = 'Tempo',

        IS_HC_NOVALIX_NUIT = false,

        IS_HC_NOVALIX_APREM = false,

        COULEUR_TEMPO = null

    } = payload;

  

    msg.commands = msg.commands || [];

  

    // ============================================================

    // ✅ V8 : MODE SÉCURITÉ (intégration Volvo indisponible)

    // ============================================================

    if (payload.MODE_SECURITE || NIVEAU_BATTERIE_VOITURE === null || NIVEAU_BATTERIE_VOITURE === undefined) {

        // En mode sécurité ou si capteur Volvo manquant : désactiver charge par sécurité

        node.warn(`⚠️ Charge véhicule désactivée en mode sécurité - Intégration Volvo indisponible`);

        msg.commands.push('CHARGE_VOITURE_OFF');

        payload.rapportChargeVoiture = 'Securite_Volvo_Indisponible';

        return msg;

    }

  

    // --- LOGIQUE DE FORÇAGE PRIORITAIRE ---

    if (VOITURE_CHARGE_FORCEE) {

        if (NIVEAU_BATTERIE_VOITURE >= 100) {

            msg.commands.push('CHARGE_VOITURE_OFF');

            msg.commands.push('TURN_OFF_input_boolean.voiture_charge_forcee');

        } else {

            msg.commands.push('CHARGE_VOITURE_ON');

        }

        return msg;

    }

  

    if (MAISON_INHABITEE) {

        msg.commands.push('CHARGE_VOITURE_OFF');

        payload.rapportChargeVoiture = 'Maison_Inhabitee';

        return msg;

    }

  

    // Si batterie voiture inconnue (-1), on arrête la charge par sécurité

    if (NIVEAU_BATTERIE_VOITURE === -1) {

        msg.commands.push('CHARGE_VOITURE_OFF');

        payload.rapportChargeVoiture = 'Batterie_Voiture_Inconnue';

        return msg;

    }

  

    const currentCarBattery = NIVEAU_BATTERIE_VOITURE;

  

    // Si batterie voiture >= consigne, on arrête la charge

    if (currentCarBattery >= CONSIGNE_CHARGE_VOITURE) {

        msg.commands.push('CHARGE_VOITURE_OFF');

        return msg;

    }

  

    // ========================================================

    // MODE TEMPO (ANCIEN COMPORTEMENT)

    // ========================================================

    if (MODE_FOURNISSEUR === 'Tempo') {

        const isOffPeakHours = HEURE >= 22 || HEURE < 6;

  

        switch (COULEUR_TEMPO) {

            case 'Rouge':

            case 'Bleu':

                if (isOffPeakHours) {

                    msg.commands.push('CHARGE_VOITURE_ON');

                } else {

                    msg.commands.push('CHARGE_VOITURE_OFF');

                }

                break;

            case 'Blanc':

            default:

                if (isOffPeakHours || SURPLUS_PUISSANCE > SEUIL_SURPLUS_SOLAIRE_VOITURE) {

                    msg.commands.push('CHARGE_VOITURE_ON');

                } else {

                    msg.commands.push('CHARGE_VOITURE_OFF');

                }

                break;

        }

        return msg;

    }

  

    // ========================================================

    // MODE NOVALIX (NOUVELLE STRATÉGIE)

    // ========================================================

    else if (MODE_FOURNISSEUR === 'Novalix') {

        // Charge pendant les HC ou en cas de surplus solaire suffisant

        if (IS_HC_NOVALIX_NUIT || IS_HC_NOVALIX_APREM || SURPLUS_PUISSANCE > SEUIL_SURPLUS_SOLAIRE_VOITURE) {

            msg.commands.push('CHARGE_VOITURE_ON');

        } else {

            msg.commands.push('CHARGE_VOITURE_OFF');

        }

        return msg;

    }

  

    // Par défaut (ne devrait jamais arriver)

    msg.commands.push('CHARGE_VOITURE_OFF');

    return msg;

  

} catch (e) {

    node.error(e.stack, msg);

    return null;

}

CODE DU NŒUD : PRÉPARATION RAPPORT ET FINALISATION (Fonction)

// ============================================================
// NŒUD : PRÉPARATION RAPPORT ET FINALISATION - VERSION CORRIGÉE
// ============================================================

try {
    const payload = msg.payload;
    const {
        COULEUR_TEMPO, HEURE, METEO_JOUR,
        NIVEAU_BATTERIE_VOITURE, NIVEAU_BATTERIE_MAISON, TEMP_CONFORT_HIVER, TEMP_ECO_NUIT_HIVER,
        TEMP_CONFORT_ETE, TEMP_ECO_NUIT_ETE, ACTIVATION_PRECHAUFFAGE_MATIN, HEURE_DEBUT_PRECHAUFFAGE_MATIN,
        HEURE_FIN_PRECHAUFFAGE_MATIN, ACTIVATION_PRECHAUFFAGE_SOIR, HEURE_DEBUT_PRECHAUFFAGE_SOIR,
        HEURE_FIN_PRECHAUFFAGE_SOIR, GESTION_AUTO_BUREAU,
        GESTION_AUTO_SALON, GESTION_AUTO_CH_PARENTS, GESTION_AUTO_CH_LEA, GESTION_AUTO_CH_CELIA,
        VENTILATION_AUTO_MI_SAISON, HEURE_DEBUT_VENTILATION, HEURE_FIN_VENTILATION,
        AJUSTEMENT_TEMP_DYNAMIQUE, DUREE_FORCAGE_MANUEL, SEUIL_FROID_EXTERIEUR, SEUIL_CHAUD_EXTERIEUR,
        rapportModeTemp, rapportAjustementTemp, rapportActionBatterie, TEMP_EXTERIEURE_ACTUELLE,
        TEMP_MAX_PREVUE,
        IS_NIGHT_BEFORE_RED_DAY,
        GESTION_AUTO_BATTERIE,
        CONSIGNE_CHARGE_VOITURE,
        TEMP_ECO_JOUR_ROUGE,
        VOITURE_CHARGE_FORCEE,
        SEUIL_BATTERIE_AUTO_SOLAIRE_NUIT,
        MODE_SAISONNIER,
        METEO_DEMAIN,
        TEMP_CONFORT_HIVER_AJUSTEE = null,
        TEMP_CONFORT_ETE_AJUSTEE = null,
        SENSOR_ERRORS = [],
        PROFIL_PRESENCE = 'ABSENT',
        PERSONNES_PRESENTES = [],
        MAISON_INHABITEE = false,
        HEURE_RETOUR_MODE_NORMAL = 17,
        MINUTE_RETOUR_MODE_NORMAL = 30,
        HEURE_DEBUT_PAUSE_DEJEUNER = 12,
        MINUTE_DEBUT_PAUSE_DEJEUNER = 0,
        HEURE_FIN_PAUSE_DEJEUNER = 13,
        MINUTE_FIN_PAUSE_DEJEUNER = 30,
        MINUTE = 0
    } = payload;

    msg.commands = msg.commands || [];

    const safeRapportModeTemp = rapportModeTemp || "Mode Inconnu";
    const safeRapportAjustementTemp = rapportAjustementTemp || "Aucun";

    // --- MISE À JOUR DES HELPERS ---
    const isPrechauffageMatinActif = (ACTIVATION_PRECHAUFFAGE_MATIN && HEURE >= HEURE_DEBUT_PRECHAUFFAGE_MATIN && HEURE < HEURE_FIN_PRECHAUFFAGE_MATIN);
    const isPrechauffageSoirActif = (ACTIVATION_PRECHAUFFAGE_SOIR && HEURE >= HEURE_DEBUT_PRECHAUFFAGE_SOIR && HEURE < HEURE_FIN_PRECHAUFFAGE_SOIR);
    const isPrechauffageActif = isPrechauffageMatinActif || isPrechauffageSoirActif;
    msg.commands.push(`SET_INPUT_TEXT_mode_temperature_${safeRapportModeTemp}`);
    msg.commands.push(`SET_INPUT_TEXT_prechauffage_${isPrechauffageActif ? 'On' : 'Off'}`);
    msg.commands.push(`SET_INPUT_TEXT_gestion_dynamique_${(safeRapportAjustementTemp !== "Aucun") ? `On_${AJUSTEMENT_TEMP_DYNAMIQUE}°` : 'Off'}`);
    msg.commands.push(`SET_INPUT_TEXT_marche_forcee_${(DUREE_FORCAGE_MANUEL > 0) ? `On_${DUREE_FORCAGE_MANUEL}h` : 'Off'}`);
    const allAuto = GESTION_AUTO_BUREAU && GESTION_AUTO_SALON && GESTION_AUTO_CH_PARENTS && GESTION_AUTO_CH_LEA && GESTION_AUTO_CH_CELIA;
    msg.commands.push(`SET_INPUT_TEXT_gestion_auto_${allAuto ? 'On' : 'Off'}`);
    const isTempExtAjustement = safeRapportAjustementTemp.includes("Froid") || safeRapportAjustementTemp.includes("Chaleur");
    msg.commands.push(`SET_INPUT_TEXT_utilisation_temp_exterieure_${isTempExtAjustement ? `On_[${SEUIL_FROID_EXTERIEUR},${SEUIL_CHAUD_EXTERIEUR}]` : 'Off'}`);
    msg.commands.push(`SET_INPUT_TEXT_mi_saison_${VENTILATION_AUTO_MI_SAISON ? `On_[${HEURE_DEBUT_VENTILATION}h-${HEURE_FIN_VENTILATION}h]` : 'Off'}`);

    // ============================================================
    // GÉNÉRATION DU RAPPORT DE PRÉSENCE - VERSION CORRIGÉE
    // ============================================================
    let rapportPresence = "";
    if (MAISON_INHABITEE) {
        rapportPresence = "🏚️ Maison inhabitée";
    } else {
        // Émojis par personne
        const emojiMap = {
            'YOHAN': '👤',
            'JENNY': '👩',
            'INVITE': '👥'
        };
        // Construction de la liste des personnes présentes
        const listePersonnes = PERSONNES_PRESENTES.map(p => `${emojiMap[p] || '👤'} ${p.charAt(0) + p.slice(1).toLowerCase()}`).join(', ');
        rapportPresence = `🏠 Présence détectée : ${listePersonnes}`;
        
        // Message spécifique selon le profil
        let messageSpecifique = "";
        
        // Formatage de l'heure de retour mode normal
        const heureRetourFormatee = (MINUTE_RETOUR_MODE_NORMAL === 0)
            ? `${HEURE_RETOUR_MODE_NORMAL}h`
            : `${HEURE_RETOUR_MODE_NORMAL}h${MINUTE_RETOUR_MODE_NORMAL.toString().padStart(2, '0')}`;
        
        // ✅ CALCULS TEMPORELS (AJOUTÉS)
        const minutesActuelles = HEURE * 60 + MINUTE;
        const minutesRetourNormal = HEURE_RETOUR_MODE_NORMAL * 60 + MINUTE_RETOUR_MODE_NORMAL;
        const minutesDebutPause = HEURE_DEBUT_PAUSE_DEJEUNER * 60 + MINUTE_DEBUT_PAUSE_DEJEUNER;
        const minutesFinPause = HEURE_FIN_PAUSE_DEJEUNER * 60 + MINUTE_FIN_PAUSE_DEJEUNER;
        
        const isAvantRetourNormal = minutesActuelles < minutesRetourNormal;
        const isPauseDejeuner = (minutesActuelles >= minutesDebutPause && minutesActuelles < minutesFinPause);

        // ✅ SWITCH CORRIGÉ AVEC VÉRIFICATION DE LA PAUSE
        switch(PROFIL_PRESENCE) {
            case 'YOHAN_SEUL':
                if (isPauseDejeuner) {
                    messageSpecifique = "En pause déjeuner : Bureau et Salon en confort";
                } else if (isAvantRetourNormal) {
                    messageSpecifique = `Configuration adaptée : Bureau confort, Salon économie jusqu'à ${heureRetourFormatee}`;
                }
                break;

            case 'JENNY_SEULE':
                if (isPauseDejeuner) {
                    messageSpecifique = "En pause déjeuner : Salon et Bureau en confort";
                } else if (isAvantRetourNormal) {
                    messageSpecifique = `Configuration adaptée : Salon confort, Bureau économie jusqu'à ${heureRetourFormatee}`;
                }
                break;

            case 'INVITE_SEUL':
                if (isPauseDejeuner) {
                    messageSpecifique = "En pause déjeuner : Salon et Bureau en confort";
                } else if (isAvantRetourNormal) {
                    messageSpecifique = `Configuration adaptée : Salon confort, Bureau économie jusqu'à ${heureRetourFormatee}`;
                }
                break;

            case 'COUPLE':
                messageSpecifique = "Configuration normale : Tous les espaces actifs";
                break;

            case 'MIXTE':
                messageSpecifique = "Configuration normale : Présence multiple détectée";
                break;

            default:
                messageSpecifique = "";
        }

        if (messageSpecifique) {
            rapportPresence += `\nℹ️ ${messageSpecifique}`;
        }
    }
    payload.rapportPresence = rapportPresence;

    // ============================================================
    // ENRICHISSEMENT DU PAYLOAD POUR LE RAPPORT (ANCIEN CODE)
    // ============================================================
    const salutation = (HEURE < 18) ? "Bonjour" : "Bonsoir";
    payload.rapportSalutation = `${salutation} !`;
    if (TEMP_MAX_PREVUE !== null) {
        payload.rapportMeteoActuelle = `Actuellement, il fait ${TEMP_EXTERIEURE_ACTUELLE}°C dehors. La température maximale attendue aujourd'hui est de ${TEMP_MAX_PREVUE}°C.`;
    } else {
        payload.rapportMeteoActuelle = `Actuellement, il fait ${TEMP_EXTERIEURE_ACTUELLE}°C dehors. La prévision de température maximale n'est pas disponible.`;
    }
    // Ajout de la pastille colorée selon le jour Tempo
    let pastille = '';
    if (COULEUR_TEMPO === 'Bleu') {
        pastille = '🔵 ';
    } else if (COULEUR_TEMPO === 'Blanc') {
        pastille = '⚪ ';
    } else if (COULEUR_TEMPO === 'Rouge') {
        pastille = '🔴 ';
    }
    payload.rapportTitre = `${pastille}Jour ${COULEUR_TEMPO} en mode ${safeRapportModeTemp.replace(/_/g, ' ')}`;
    payload.rapportContexte = "";
    if (isPrechauffageMatinActif) {
        payload.rapportContexte = `Le préchauffage du matin est en cours jusqu'à ${HEURE_FIN_PRECHAUFFAGE_MATIN}h.`;
    } else if (isPrechauffageSoirActif) {
        payload.rapportContexte = `Le préchauffage du soir est en cours jusqu'à ${HEURE_FIN_PRECHAUFFAGE_SOIR}h.`;
    }
  
    // --- LOGIQUE RAPPORT CHAUFFAGE ---
    if (['Securite Batterie 16h', 'Attente Solaire Rouge'].includes(safeRapportModeTemp)) {
        if (!payload.rapportChauffage) {
            payload.rapportChauffage = "Mode sécurité activé.";
        }
    }
    // ✅ GESTION DIFFÉRENCIÉE - YOHAN SEUL
    else if (safeRapportModeTemp === 'Yohan_Seul_Pause_Dejeuner') {
        const tempConfortAffichee = (safeRapportAjustementTemp !== "Aucun" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;
        payload.rapportChauffage = `Pause déjeuner : Bureau et Salon en confort à ${tempConfortAffichee.toFixed(1)}°C, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;
    } else if (safeRapportModeTemp === 'Yohan_Seul_Bureau') {
        const tempConfortAffichee = (safeRapportAjustementTemp !== "Aucun" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;
        payload.rapportChauffage = `Bureau en confort à ${tempConfortAffichee.toFixed(1)}°C, Salon en économie à ${TEMP_ECO_NUIT_HIVER}°C, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;
    } else if (safeRapportModeTemp === 'Yohan_Seul_Normal') {
        const tempConfortAffichee = (safeRapportAjustementTemp !== "Aucun" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;
        const minutesActuelles = HEURE * 60 + MINUTE;
        const minutesRetourNormal = HEURE_RETOUR_MODE_NORMAL * 60 + MINUTE_RETOUR_MODE_NORMAL;
        const mentionRetour = (minutesActuelles >= minutesRetourNormal && minutesActuelles < minutesRetourNormal + 10) ? ' (retour mode normal)' : '';
        payload.rapportChauffage = `Bureau et Salon en confort à ${tempConfortAffichee.toFixed(1)}°C${mentionRetour}, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;
    }
    // ✅ GESTION DIFFÉRENCIÉE - JENNY SEULE
    else if (safeRapportModeTemp === 'Jenny_Seule_Pause_Dejeuner') {
        const tempConfortAffichee = (safeRapportAjustementTemp !== "Aucun" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;
        payload.rapportChauffage = `Pause déjeuner : Salon et Bureau en confort à ${tempConfortAffichee.toFixed(1)}°C, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;
    } else if (safeRapportModeTemp === 'Jenny_Seule_Salon') {
        const tempConfortAffichee = (safeRapportAjustementTemp !== "Aucun" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;
        payload.rapportChauffage = `Salon en confort à ${tempConfortAffichee.toFixed(1)}°C, Bureau en économie à ${TEMP_ECO_NUIT_HIVER}°C, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;
    } else if (safeRapportModeTemp === 'Jenny_Seule_Normal') {
        const tempConfortAffichee = (safeRapportAjustementTemp !== "Aucun" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;
        const minutesActuelles = HEURE * 60 + MINUTE;
        const minutesRetourNormal = HEURE_RETOUR_MODE_NORMAL * 60 + MINUTE_RETOUR_MODE_NORMAL;
        const mentionRetour = (minutesActuelles >= minutesRetourNormal && minutesActuelles < minutesRetourNormal + 10) ? ' (retour mode normal)' : '';
        payload.rapportChauffage = `Salon et Bureau en confort à ${tempConfortAffichee.toFixed(1)}°C${mentionRetour}, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;
    }
    // ✅ GESTION DIFFÉRENCIÉE - INVITÉ SEUL
    else if (safeRapportModeTemp === 'Invite_Seul_Pause_Dejeuner') {
        const tempConfortAffichee = (safeRapportAjustementTemp !== "Aucun" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;
        payload.rapportChauffage = `Pause déjeuner : Salon et Bureau en confort à ${tempConfortAffichee.toFixed(1)}°C, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;
    } else if (safeRapportModeTemp === 'Invite_Seul_Salon') {
        const tempConfortAffichee = (safeRapportAjustementTemp !== "Aucun" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;
        payload.rapportChauffage = `Salon en confort à ${tempConfortAffichee.toFixed(1)}°C, Bureau en économie à ${TEMP_ECO_NUIT_HIVER}°C, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;
    } else if (safeRapportModeTemp === 'Invite_Seul_Normal') {
        const tempConfortAffichee = (safeRapportAjustementTemp !== "Aucun" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;
        const minutesActuelles = HEURE * 60 + MINUTE;
        const minutesRetourNormal = HEURE_RETOUR_MODE_NORMAL * 60 + MINUTE_RETOUR_MODE_NORMAL;
        const mentionRetour = (minutesActuelles >= minutesRetourNormal && minutesActuelles < minutesRetourNormal + 10) ? ' (retour mode normal)' : '';
        payload.rapportChauffage = `Salon et Bureau en confort à ${tempConfortAffichee.toFixed(1)}°C${mentionRetour}, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;
    }
    // ✅ MODES STANDARDS
    else if (safeRapportModeTemp === 'Hiver_Confort' || safeRapportModeTemp === 'Hiver Confort') {
        const tempConfortAffichee = (safeRapportAjustementTemp !== "Aucun" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;
        const ajustementText = (safeRapportAjustementTemp !== "Aucun") ? ` (ajustée via ${safeRapportAjustementTemp.replace(/_/g, ' ')})` : '';
        payload.rapportChauffage = `Les pièces de vie sont réglées autour de ${tempConfortAffichee.toFixed(1)}°C${ajustementText} et les chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;
    } else if (safeRapportModeTemp === 'Nuit_Eco' || safeRapportModeTemp === 'Nuit Eco') {
        payload.rapportChauffage = `Les chambres sont chauffées à ${TEMP_ECO_NUIT_HIVER}°C. Le chauffage des pièces de vie est éteint.`;
    } else if (safeRapportModeTemp === 'Ete_Confort' || safeRapportModeTemp === 'Ete Confort') {
        const tempConfortAffichee = (safeRapportAjustementTemp !== "Aucun" && TEMP_CONFORT_ETE_AJUSTEE !== null) ? TEMP_CONFORT_ETE_AJUSTEE : TEMP_CONFORT_ETE;
        const ajustementText = (safeRapportAjustementTemp !== "Aucun") ? ` (ajustée via ${safeRapportAjustementTemp.replace(/_/g, ' ')})` : '';
        payload.rapportChauffage = `Toutes les pièces sont climatisées autour de ${tempConfortAffichee.toFixed(1)}°C${ajustementText}.`;
    } else {
        if (!payload.rapportChauffage) {
            payload.rapportChauffage = "Le chauffage et la climatisation sont désactivés.";
        }
    }
  
    // --- LOGIQUE RAPPORT VOITURE ---
    const isChargingCommandSent = msg.commands.includes('CHARGE_VOITURE_ON');
    if (VOITURE_CHARGE_FORCEE && isChargingCommandSent) {
        payload.rapportVoiture = "Charge forcée manuelle en cours.";
    } else if (NIVEAU_BATTERIE_VOITURE === -1) {
        payload.rapportVoiture = "Voiture non connectée.";
    } else if (NIVEAU_BATTERIE_VOITURE >= CONSIGNE_CHARGE_VOITURE) {
        payload.rapportVoiture = `La batterie a atteint la consigne de ${CONSIGNE_CHARGE_VOITURE}%, la charge est arrêtée.`;
    } else if (isChargingCommandSent) {
        payload.rapportVoiture = `Charge automatique en cours pour atteindre ${CONSIGNE_CHARGE_VOITURE}%.`;
    } else {
        payload.rapportVoiture = `Charge en attente d'une opportunité (heures creuses/solaire) pour atteindre ${CONSIGNE_CHARGE_VOITURE}%.`;
    }
  
    // --- LOGIQUE RAPPORT PISCINE ---
    payload.rapportPiscine = msg.commands.includes('MODE_AUTO_PISCINE_ON') ? "Le mode automatique est activé." : "Le mode automatique est désactivé.";
  
    // --- LOGIQUE RAPPORT BATTERIE ---
    if (GESTION_AUTO_BATTERIE) {
        payload.rapportBatterieLigne1 = `La batterie est en mode ${rapportActionBatterie.replace(/_/g, ' ')} (actuellement à ${NIVEAU_BATTERIE_MAISON}%).`;
        payload.rapportBatterieLigne2 = "";
    } else {
        payload.rapportBatterieLigne1 = `La gestion de la batterie est en mode forcé : ${rapportActionBatterie.replace(/_/g, ' ')} (actuellement à ${NIVEAU_BATTERIE_MAISON}%).`;
        payload.rapportBatterieLigne2 = "";
    }
  
    // --- AJOUT DE LA SAISON AU RAPPORT ---
    payload.rapportSaison = `Saison détectée : **${MODE_SAISONNIER.replace(/_/g, ' ')}**`;
  
    // Création des deux messages de sortie
    const msg_actions = { commands: [...new Set(msg.commands)] };
    const msg_rapport = { payload: payload };
  
    return [msg_actions, msg_rapport];
  
} catch (e) {
    node.error(e.stack, msg);
    return null;
}

FILTRE RAPPORT QUOTIDIEN: (Fonction)

// ============================================================

// NŒUD : PRÉPARATION RAPPORT ET FINALISATION - VERSION V8

// Type: Function

// ============================================================

// Ce nœud génère le rapport quotidien avec toutes les infos

// et met à jour les helpers d'état dans Home Assistant.

//

// ✅ NOUVEAUTÉS V8 :

// - Mention du fournisseur d'électricité actif

// - Rapports adaptés aux stratégies Tempo/Novalix

// - Suppression des mentions jour Rouge pour Novalix

// ============================================================

  

try {

    const payload = msg.payload;

    const {

        HEURE, METEO_JOUR,

        NIVEAU_BATTERIE_VOITURE, NIVEAU_BATTERIE_MAISON, TEMP_CONFORT_HIVER, TEMP_ECO_NUIT_HIVER,

        TEMP_CONFORT_ETE, TEMP_ECO_NUIT_ETE, ACTIVATION_PRECHAUFFAGE_MATIN, HEURE_DEBUT_PRECHAUFFAGE_MATIN,

        HEURE_FIN_PRECHAUFFAGE_MATIN, ACTIVATION_PRECHAUFFAGE_SOIR, HEURE_DEBUT_PRECHAUFFAGE_SOIR,

        HEURE_FIN_PRECHAUFFAGE_SOIR, GESTION_AUTO_BUREAU,

        GESTION_AUTO_SALON, GESTION_AUTO_CH_PARENTS, GESTION_AUTO_CH_LEA, GESTION_AUTO_CH_CELIA,

        VENTILATION_AUTO_MI_SAISON, HEURE_DEBUT_VENTILATION, HEURE_FIN_VENTILATION,

        AJUSTEMENT_TEMP_DYNAMIQUE, DUREE_FORCAGE_MANUEL, SEUIL_FROID_EXTERIEUR, SEUIL_CHAUD_EXTERIEUR,

        rapportModeTemp, rapportAjustementTemp, rapportActionBatterie, TEMP_EXTERIEURE_ACTUELLE,

        TEMP_MAX_PREVUE, IS_NIGHT_BEFORE_RED_DAY, GESTION_AUTO_BATTERIE,

        CONSIGNE_CHARGE_VOITURE, TEMP_ECO_JOUR_ROUGE, VOITURE_CHARGE_FORCEE,

        SEUIL_BATTERIE_AUTO_SOLAIRE_NUIT, MODE_SAISONNIER, METEO_DEMAIN,

        TEMP_CONFORT_HIVER_AJUSTEE = null, TEMP_CONFORT_ETE_AJUSTEE = null,

        SENSOR_ERRORS = [], PROFIL_PRESENCE = 'ABSENT', PERSONNES_PRESENTES = [],

        MAISON_INHABITEE = false, HEURE_RETOUR_MODE_NORMAL = 17,

        MINUTE_RETOUR_MODE_NORMAL = 30, HEURE_DEBUT_PAUSE_DEJEUNER = 12,

        MINUTE_DEBUT_PAUSE_DEJEUNER = 0, HEURE_FIN_PAUSE_DEJEUNER = 13,

        MINUTE_FIN_PAUSE_DEJEUNER = 30, MINUTE = 0,

        // ✅ NOUVEAUX PARAMÈTRES V8

        MODE_FOURNISSEUR = 'Tempo',

        COULEUR_TEMPO = null

    } = payload;

  

    msg.commands = msg.commands || [];

  

    const safeRapportModeTemp = rapportModeTemp || "Mode Inconnu";

    const safeRapportAjustementTemp = rapportAjustementTemp || "Aucun";

  

    // --- MISE À JOUR DES HELPERS ---

    const isPrechauffageMatinActif = (ACTIVATION_PRECHAUFFAGE_MATIN && HEURE >= HEURE_DEBUT_PRECHAUFFAGE_MATIN && HEURE < HEURE_FIN_PRECHAUFFAGE_MATIN);

    const isPrechauffageSoirActif = (ACTIVATION_PRECHAUFFAGE_SOIR && HEURE >= HEURE_DEBUT_PRECHAUFFAGE_SOIR && HEURE < HEURE_FIN_PRECHAUFFAGE_SOIR);

    const isPrechauffageActif = isPrechauffageMatinActif || isPrechauffageSoirActif;

    msg.commands.push(`SET_INPUT_TEXT_mode_temperature_${safeRapportModeTemp}`);

    msg.commands.push(`SET_INPUT_TEXT_prechauffage_${isPrechauffageActif ? 'On' : 'Off'}`);

    msg.commands.push(`SET_INPUT_TEXT_gestion_dynamique_${(safeRapportAjustementTemp !== "Aucun") ? `On_${AJUSTEMENT_TEMP_DYNAMIQUE}°` : 'Off'}`);

    msg.commands.push(`SET_INPUT_TEXT_marche_forcee_${(DUREE_FORCAGE_MANUEL > 0) ? `On_${DUREE_FORCAGE_MANUEL}h` : 'Off'}`);

    const allAuto = GESTION_AUTO_BUREAU && GESTION_AUTO_SALON && GESTION_AUTO_CH_PARENTS && GESTION_AUTO_CH_LEA && GESTION_AUTO_CH_CELIA;

    msg.commands.push(`SET_INPUT_TEXT_gestion_auto_${allAuto ? 'On' : 'Off'}`);

    const isTempExtAjustement = safeRapportAjustementTemp.includes("Froid") || safeRapportAjustementTemp.includes("Chaleur");

    msg.commands.push(`SET_INPUT_TEXT_utilisation_temp_exterieure_${isTempExtAjustement ? `On_[${SEUIL_FROID_EXTERIEUR},${SEUIL_CHAUD_EXTERIEUR}]` : 'Off'}`);

    msg.commands.push(`SET_INPUT_TEXT_mi_saison_${VENTILATION_AUTO_MI_SAISON ? `On_[${HEURE_DEBUT_VENTILATION}h-${HEURE_FIN_VENTILATION}h]` : 'Off'}`);

  

    // ============================================================

    // GÉNÉRATION DU RAPPORT DE PRÉSENCE

    // ============================================================

    let rapportPresence = "";

    if (MAISON_INHABITEE) {

        rapportPresence = "🏚️ Maison inhabitée";

    } else {

        const emojiMap = {

            'YOHAN': '👤',

            'JENNY': '👩',

            'INVITE': '👥'

        };

        const listePersonnes = PERSONNES_PRESENTES.map(p => `${emojiMap[p] || '👤'} ${p.charAt(0) + p.slice(1).toLowerCase()}`).join(', ');

        rapportPresence = `🏠 Présence détectée : ${listePersonnes}`;

        let messageSpecifique = "";

        const heureRetourFormatee = (MINUTE_RETOUR_MODE_NORMAL === 0)

            ? `${HEURE_RETOUR_MODE_NORMAL}h`

            : `${HEURE_RETOUR_MODE_NORMAL}h${MINUTE_RETOUR_MODE_NORMAL.toString().padStart(2, '0')}`;

        const minutesActuelles = HEURE * 60 + MINUTE;

        const minutesRetourNormal = HEURE_RETOUR_MODE_NORMAL * 60 + MINUTE_RETOUR_MODE_NORMAL;

        const minutesDebutPause = HEURE_DEBUT_PAUSE_DEJEUNER * 60 + MINUTE_DEBUT_PAUSE_DEJEUNER;

        const minutesFinPause = HEURE_FIN_PAUSE_DEJEUNER * 60 + MINUTE_FIN_PAUSE_DEJEUNER;

        const isAvantRetourNormal = minutesActuelles < minutesRetourNormal;

        const isPauseDejeuner = (minutesActuelles >= minutesDebutPause && minutesActuelles < minutesFinPause);

  

        switch(PROFIL_PRESENCE) {

            case 'YOHAN_SEUL':

                if (isPauseDejeuner) {

                    messageSpecifique = "En pause déjeuner : Bureau et Salon en confort";

                } else if (isAvantRetourNormal) {

                    messageSpecifique = `Configuration adaptée : Bureau confort, Salon économie jusqu'à ${heureRetourFormatee}`;

                }

                break;

  

            case 'JENNY_SEULE':

                if (isPauseDejeuner) {

                    messageSpecifique = "En pause déjeuner : Salon et Bureau en confort";

                } else if (isAvantRetourNormal) {

                    messageSpecifique = `Configuration adaptée : Salon confort, Bureau économie jusqu'à ${heureRetourFormatee}`;

                }

                break;

  

            case 'INVITE_SEUL':

                if (isPauseDejeuner) {

                    messageSpecifique = "En pause déjeuner : Salon et Bureau en confort";

                } else if (isAvantRetourNormal) {

                    messageSpecifique = `Configuration adaptée : Salon confort, Bureau économie jusqu'à ${heureRetourFormatee}`;

                }

                break;

  

            case 'COUPLE':

                messageSpecifique = "Configuration normale : Tous les espaces actifs";

                break;

  

            case 'MIXTE':

                messageSpecifique = "Configuration normale : Présence multiple détectée";

                break;

  

            default:

                messageSpecifique = "";

        }

  

        if (messageSpecifique) {

            rapportPresence += `\nℹ️ ${messageSpecifique}`;

        }

    }

    payload.rapportPresence = rapportPresence;

  

    // ============================================================

    // ENRICHISSEMENT DU PAYLOAD POUR LE RAPPORT

    // ============================================================

    const salutation = (HEURE < 18) ? "Bonjour" : "Bonsoir";

    payload.rapportSalutation = `${salutation} !`;

    // ✅ V8 : Mention du fournisseur d'électricité

    payload.rapportFournisseur = `📊 Fournisseur : **${MODE_FOURNISSEUR}**`;

    if (TEMP_MAX_PREVUE !== null) {

        payload.rapportMeteoActuelle = `Actuellement, il fait ${TEMP_EXTERIEURE_ACTUELLE}°C dehors. La température maximale attendue aujourd'hui est de ${TEMP_MAX_PREVUE}°C.`;

    } else {

        payload.rapportMeteoActuelle = `Actuellement, il fait ${TEMP_EXTERIEURE_ACTUELLE}°C dehors. La prévision de température maximale n'est pas disponible.`;

    }

    // ✅ V8 : Titre adapté selon le fournisseur

    let pastille = '';

    if (MODE_FOURNISSEUR === 'Tempo' && COULEUR_TEMPO) {

        if (COULEUR_TEMPO === 'Bleu') pastille = '🔵 ';

        else if (COULEUR_TEMPO === 'Blanc') pastille = '⚪ ';

        else if (COULEUR_TEMPO === 'Rouge') pastille = '🔴 ';

        payload.rapportTitre = `${pastille}Jour ${COULEUR_TEMPO} en mode ${safeRapportModeTemp.replace(/_/g, ' ')}`;

    } else {

        payload.rapportTitre = `Mode ${safeRapportModeTemp.replace(/_/g, ' ')}`;

    }

    payload.rapportContexte = "";

    if (isPrechauffageMatinActif) {

        payload.rapportContexte = `Le préchauffage du matin est en cours jusqu'à ${HEURE_FIN_PRECHAUFFAGE_MATIN}h.`;

    } else if (isPrechauffageSoirActif) {

        payload.rapportContexte = `Le préchauffage du soir est en cours jusqu'à ${HEURE_FIN_PRECHAUFFAGE_SOIR}h.`;

    }

    // --- LOGIQUE RAPPORT CHAUFFAGE ---

    if (['Securite Batterie 16h', 'Attente Solaire Rouge'].includes(safeRapportModeTemp)) {

        if (!payload.rapportChauffage) {

            payload.rapportChauffage = "Mode sécurité activé.";

        }

    }

    else if (safeRapportModeTemp === 'Yohan_Seul_Pause_Dejeuner') {

        const tempConfortAffichee = (safeRapportAjustementTemp !== "Aucun" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;

        payload.rapportChauffage = `Pause déjeuner : Bureau et Salon en confort à ${tempConfortAffichee.toFixed(1)}°C, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;

    } else if (safeRapportModeTemp === 'Yohan_Seul_Bureau') {

        const tempConfortAffichee = (safeRapportAjustementTemp !== "Aucun" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;

        payload.rapportChauffage = `Bureau en confort à ${tempConfortAffichee.toFixed(1)}°C, Salon en économie à ${TEMP_ECO_NUIT_HIVER}°C, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;

    } else if (safeRapportModeTemp === 'Yohan_Seul_Normal') {

        const tempConfortAffichee = (safeRapportAjustementTemp !== "Aucun" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;

        const minutesActuelles = HEURE * 60 + MINUTE;

        const minutesRetourNormal = HEURE_RETOUR_MODE_NORMAL * 60 + MINUTE_RETOUR_MODE_NORMAL;

        const mentionRetour = (minutesActuelles >= minutesRetourNormal && minutesActuelles < minutesRetourNormal + 10) ? ' (retour mode normal)' : '';

        payload.rapportChauffage = `Bureau et Salon en confort à ${tempConfortAffichee.toFixed(1)}°C${mentionRetour}, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;

    }

    else if (safeRapportModeTemp === 'Jenny_Seule_Pause_Dejeuner') {

        const tempConfortAffichee = (safeRapportAjustementTemp !== "Aucun" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;

        payload.rapportChauffage = `Pause déjeuner : Salon et Bureau en confort à ${tempConfortAffichee.toFixed(1)}°C, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;

    } else if (safeRapportModeTemp === 'Jenny_Seule_Salon') {

        const tempConfortAffichee = (safeRapportAjustementTemp !== "Aucun" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;

        payload.rapportChauffage = `Salon en confort à ${tempConfortAffichee.toFixed(1)}°C, Bureau en économie à ${TEMP_ECO_NUIT_HIVER}°C, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;

    } else if (safeRapportModeTemp === 'Jenny_Seule_Normal') {

        const tempConfortAffichee = (safeRapportAjustementTemp !== "Aucun" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;

        const minutesActuelles = HEURE * 60 + MINUTE;

        const minutesRetourNormal = HEURE_RETOUR_MODE_NORMAL * 60 + MINUTE_RETOUR_MODE_NORMAL;

        const mentionRetour = (minutesActuelles >= minutesRetourNormal && minutesActuelles < minutesRetourNormal + 10) ? ' (retour mode normal)' : '';

        payload.rapportChauffage = `Salon et Bureau en confort à ${tempConfortAffichee.toFixed(1)}°C${mentionRetour}, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;

    }

    else if (safeRapportModeTemp === 'Invite_Seul_Pause_Dejeuner') {

        const tempConfortAffichee = (safeRapportAjustementTemp !== "Aucun" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;

        payload.rapportChauffage = `Pause déjeuner : Salon et Bureau en confort à ${tempConfortAffichee.toFixed(1)}°C, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;

    } else if (safeRapportModeTemp === 'Invite_Seul_Salon') {

        const tempConfortAffichee = (safeRapportAjustementTemp !== "Aucun" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;

        payload.rapportChauffage = `Salon en confort à ${tempConfortAffichee.toFixed(1)}°C, Bureau en économie à ${TEMP_ECO_NUIT_HIVER}°C, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;

    } else if (safeRapportModeTemp === 'Invite_Seul_Normal') {

        const tempConfortAffichee = (safeRapportAjustementTemp !== "Aucun" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;

        const minutesActuelles = HEURE * 60 + MINUTE;

        const minutesRetourNormal = HEURE_RETOUR_MODE_NORMAL * 60 + MINUTE_RETOUR_MODE_NORMAL;

        const mentionRetour = (minutesActuelles >= minutesRetourNormal && minutesActuelles < minutesRetourNormal + 10) ? ' (retour mode normal)' : '';

        payload.rapportChauffage = `Salon et Bureau en confort à ${tempConfortAffichee.toFixed(1)}°C${mentionRetour}, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;

    }

    else if (safeRapportModeTemp === 'Hiver_Confort' || safeRapportModeTemp === 'Hiver Confort') {

        const tempConfortAffichee = (safeRapportAjustementTemp !== "Aucun" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;

        const ajustementText = (safeRapportAjustementTemp !== "Aucun") ? ` (ajustée via ${safeRapportAjustementTemp.replace(/_/g, ' ')})` : '';

        payload.rapportChauffage = `Les pièces de vie sont réglées autour de ${tempConfortAffichee.toFixed(1)}°C${ajustementText} et les chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;

    } else if (safeRapportModeTemp === 'Nuit_Eco' || safeRapportModeTemp === 'Nuit Eco') {

        payload.rapportChauffage = `Les chambres sont chauffées à ${TEMP_ECO_NUIT_HIVER}°C. Le chauffage des pièces de vie est éteint.`;

    } else if (safeRapportModeTemp === 'Ete_Confort' || safeRapportModeTemp === 'Ete Confort') {

        const tempConfortAffichee = (safeRapportAjustementTemp !== "Aucun" && TEMP_CONFORT_ETE_AJUSTEE !== null) ? TEMP_CONFORT_ETE_AJUSTEE : TEMP_CONFORT_ETE;

        const ajustementText = (safeRapportAjustementTemp !== "Aucun") ? ` (ajustée via ${safeRapportAjustementTemp.replace(/_/g, ' ')})` : '';

        payload.rapportChauffage = `Toutes les pièces sont climatisées autour de ${tempConfortAffichee.toFixed(1)}°C${ajustementText}.`;

    } else {

        if (!payload.rapportChauffage) {

            payload.rapportChauffage = "Le chauffage et la climatisation sont désactivés.";

        }

    }

    // --- LOGIQUE RAPPORT VOITURE ---

    const isChargingCommandSent = msg.commands.includes('CHARGE_VOITURE_ON');

    if (VOITURE_CHARGE_FORCEE && isChargingCommandSent) {

        payload.rapportVoiture = "Charge forcée manuelle en cours.";

    } else if (NIVEAU_BATTERIE_VOITURE === -1) {

        payload.rapportVoiture = "Voiture non connectée.";

    } else if (NIVEAU_BATTERIE_VOITURE >= CONSIGNE_CHARGE_VOITURE) {

        payload.rapportVoiture = `La batterie a atteint la consigne de ${CONSIGNE_CHARGE_VOITURE}%, la charge est arrêtée.`;

    } else if (isChargingCommandSent) {

        payload.rapportVoiture = `Charge automatique en cours pour atteindre ${CONSIGNE_CHARGE_VOITURE}%.`;

    } else {

        payload.rapportVoiture = `Charge en attente d'une opportunité (heures creuses/solaire) pour atteindre ${CONSIGNE_CHARGE_VOITURE}%.`;

    }

    // --- LOGIQUE RAPPORT PISCINE ---

    payload.rapportPiscine = msg.commands.includes('MODE_AUTO_PISCINE_ON') ? "Le mode automatique est activé." : "Le mode automatique est désactivé.";

    // --- LOGIQUE RAPPORT BATTERIE ---

    if (GESTION_AUTO_BATTERIE) {

        payload.rapportBatterieLigne1 = `La batterie est en mode ${rapportActionBatterie.replace(/_/g, ' ')} (actuellement à ${NIVEAU_BATTERIE_MAISON}%).`;

        payload.rapportBatterieLigne2 = "";

    } else {

        payload.rapportBatterieLigne1 = `La gestion de la batterie est en mode forcé : ${rapportActionBatterie.replace(/_/g, ' ')} (actuellement à ${NIVEAU_BATTERIE_MAISON}%).`;

        payload.rapportBatterieLigne2 = "";

    }

    // --- AJOUT DE LA SAISON AU RAPPORT ---

    payload.rapportSaison = `Saison détectée : **${MODE_SAISONNIER.replace(/_/g, ' ')}**`;

    // Création des deux messages de sortie

    const msg_actions = { commands: [...new Set(msg.commands)] };

    const msg_rapport = { payload: payload };

    return [msg_actions, msg_rapport];

} catch (e) {

    node.error(e.stack, msg);

    return null;

}

CODE DU NŒUD : FORMATAGE DU RAPPORT FINAL (Fonction)

// ============================================================

// NŒUD : FORMATAGE DU RAPPORT FINAL - VERSION V8

// Type: Function

// ============================================================

// Ce nœud génère le rapport final en Markdown avec toutes

// les informations de la journée.

//

// ✅ NOUVEAUTÉS V8 :

// - Affichage du fournisseur d'électricité actif

// ============================================================

  

try {

    const payload = msg.payload;

  

    // Validation des données essentielles

    if (!payload || typeof payload !== 'object') {

        throw new Error("Payload invalide ou manquant");

    }

  

    let report = "";

    report += `${payload.rapportSalutation || 'Bonjour !'}\n`;

    report += `${payload.rapportMeteoActuelle || 'Informations météo non disponibles.'}\n\n`;

    // ✅ V8 : Affichage du fournisseur

    if (payload.rapportFournisseur) {

        report += `${payload.rapportFournisseur}\n`;

    }

    report += `**${payload.rapportTitre || 'Rapport quotidien'}**\n`;

    report += `${payload.rapportSaison || ''}\n`;

    // Rapport de présence

    if (payload.rapportPresence) {

        report += `\n${payload.rapportPresence}\n\n`;

    } else {

        report += '\n';

    }

  

    if (payload.rapportContexte) {

        report += `${payload.rapportContexte}\n\n`;

    }

  

    if (payload.rapportAlertes) {

        report += `**⚠️ Alertes Système**\n${payload.rapportAlertes}\n\n`;

    }

  

    report += `**🌡️ Chauffage**\n${payload.rapportChauffage || 'Information non disponible'}\n\n`;

    report += `**🚗 Voiture Électrique**\n${payload.rapportVoiture || 'Information non disponible'}\n\n`;

    report += `**🏊 Piscine**\n${payload.rapportPiscine || 'Information non disponible'}\n\n`;

    report += `**🔋 Batterie Maison**\n${payload.rapportBatterieLigne1 || 'Information non disponible'}\n`;

    if (payload.rapportBatterieLigne2) {

        report += `${payload.rapportBatterieLigne2}\n`;

    }

  

    if (msg.quote && msg.quote.citation) {

        report += `\n**✨ La pensée du jour...**\n`;

        report += `"${msg.quote.citation}"`;

        if (msg.quote.infos && msg.quote.infos.personnage) {

            report += `\n${msg.quote.infos.personnage}`;

        }

    }

  

    // 19 phrases de conclusion

    const conclusions = [

        "Passez une excellente journée !",

        "Votre maison veille sur vous.",

        "Toute l'équipe domotique vous souhaite une bonne journée.",

        "Optimisez votre confort, votre maison s'en occupe.",

        "Une domotique performante pour une vie simplifiée.",

        "Profitez de votre journée, votre système intelligent gère le reste.",

        "Économisez l'énergie, votre maison pense à tout.",

        "La maison intelligente travaille pour votre bien-être.",

        "Votre écosystème domotique est à votre service.",

        "Confort optimal garanti par votre système automatisé.",

        "Laissez la technologie simplifier votre quotidien.",

        "Votre maison connectée anticipe vos besoins.",

        "L'intelligence artificielle au service de votre confort.",

        "Détendez-vous, votre habitat intelligent vous facilite la vie.",

        "Une gestion énergétique optimale pour des économies durables.",

        "Votre maison s'adapte à vos habitudes pour un confort sur mesure.",

        "Technologie et confort se rejoignent pour votre bien-être.",

        "Profitez d'une maison qui pense avant vous.",

        "L'avenir de l'habitat commence aujourd'hui chez vous."

    ];

    report += "\n\n---\n" + conclusions[Math.floor(Math.random() * conclusions.length)];

  

    msg.payload = report;

    return msg;

  

} catch (e) {

    node.error("Erreur lors du formatage du rapport : " + e.message, msg);

    msg.payload = "⚠️ Une erreur s'est produite lors de la génération du rapport quotidien.";

    return msg;

}

CODE DU NŒUD : DISPATCHER DE THERMOSTAT (Fonction)

// ### CODE DU NŒUD : DISPATCHER DE THERMOSTAT - AMÉLIORÉ ###

try {
    const command = msg.payload;

    if (!command || typeof command !== 'string') {
        node.warn("Commande invalide reçue par le dispatcher thermostat");
        return null;
    }

    const entityMap = { 
        'BUREAU': 'climate.bureau', 
        'SALON': 'climate.salon', 
        'CHAMBRE_PARENTS': 'climate.parents', 
        'CHAMBRE_LEA': 'climate.lea', 
        'CHAMBRE_CELIA': 'climate.celia' 
    };

    const modeMap = { 
        'CHAUFFAGE': 'heat', 
        'CLIMATISATION': 'cool', 
        'VENTILATION': 'fan_only' 
    };

    let tempRegex = /^(.+)_TEMP_(\d+\.?\d*)$/; 
    let match = command.match(tempRegex);

    if (match) { 
        const piece = match[1]; 
        const temperature = parseFloat(match[2]); 
        const entityId = entityMap[piece]; 
        if (entityId) { 
            msg.payload = { 
                type: "call_service", domain: "climate", service: "set_temperature", 
                service_data: { temperature: temperature }, 
                target: { entity_id: entityId } 
            }; 
            return msg; 
        } 
    }

    let onOffRegex = /^THERMOSTAT_(.+)_(ON|OFF)$/; 
    match = command.match(onOffRegex);

    if (match) { 
        const piece = match[1]; 
        const state = match[2]; 
        const entityId = entityMap[piece]; 
        const service = (state === 'ON') ? 'turn_on' : 'turn_off'; 
        if (entityId) { 
            msg.payload = { 
                type: "call_service", domain: "climate", service: service, 
                target: { entity_id: entityId } 
            }; 
            return msg; 
        } 
    }

    let modeRegex = /^(.+)_MODE_(CHAUFFAGE|CLIMATISATION|VENTILATION)$/; 
    match = command.match(modeRegex);

    if (match) { 
        const piece = match[1]; 
        const mode = match[2]; 
        const entityId = entityMap[piece]; 
        const hvacMode = modeMap[mode];
        if (entityId && hvacMode) { 
            msg.payload = { 
                type: "call_service", domain: "climate", service: "set_hvac_mode", 
                service_data: { hvac_mode: hvacMode }, 
                target: { entity_id: entityId } 
            }; 
            return msg; 
        } 
    }

    return null;

} catch (e) {
    node.error("Erreur dans le dispatcher thermostat : " + e.message, msg);
    return null;
}

CODE DU NŒUD : DISPATCHER TIMESTAMPS (Fonction)

// ### CODE DU NŒUD : DISPATCHER TIMESTAMPS - AMÉLIORÉ ###

try {
    const command = msg.payload;
    
    if (!command || typeof command !== 'string') {
        node.warn("Commande invalide reçue par le dispatcher timestamps");
        return null;
    }
    
    const regex = /^SET_INPUT_TEXT_(override_timestamp_[a-zA-Z0-9_]+)_(.*)$/;
    const match = command.match(regex);

    if (match) {
        const helperName = match[1];
        const value = match[2];
        const entityId = `input_text.${helperName}`;
        msg.payload = {
            type: "call_service",
            domain: "input_text",
            service: "set_value",
            service_data: {
                value: value
            },
            target: {
                entity_id: entityId
            }
        };
        return msg;
    }

    return null;

} catch (e) {
    node.error("Erreur dans le dispatcher timestamps : " + e.message, msg);
    return null;
}

CODE DU NŒUD : DISPATCHER INPUT NUMBER (Fonction)

// ### CODE DU NŒUD : DISPATCHER INPUT NUMBER - AMÉLIORÉ ###

try {
    const command = msg.payload;
    
    if (!command || typeof command !== 'string') {
        node.warn("Commande invalide reçue par le dispatcher input_number");
        return null;
    }
    
    const regex = /^SET_INPUT_NUMBER_([a-zA-Z0-9_]+)_(-?\d+\.?\d*)$/;
    const match = command.match(regex);

    if (match) {
        const helperName = match[1];
        const value = parseFloat(match[2]);
        
        if (isNaN(value)) {
            node.warn(`Valeur numérique invalide pour ${helperName}: ${match[2]}`);
            return null;
        }
        
        const entityId = `input_number.${helperName}`;

        msg.payload = {
            type: "call_service",
            domain: "input_number",
            service: "set_value",
            service_data: {
                value: value
            },
            target: {
                entity_id: entityId
            }
        };
        return msg;
    }

    return null;

} catch (e) {
    node.error("Erreur dans le dispatcher input_number : " + e.message, msg);
    return null;
}

CODE DU NŒUD : DISPATCHER INPUT TEXT (Fonction)

// ### CODE DU NŒUD : DISPATCHER INPUT TEXT - AMÉLIORÉ ###

try {
    const command = msg.payload && typeof msg.payload === 'string' ? msg.payload.trim() : '';
    
    if (!command) {
        node.warn("Commande invalide ou vide reçue par le dispatcher input_text");
        return null;
    }

    const helperNames = [
        'mode_temperature',
        'prechauffage',
        'gestion_dynamique',
        'marche_forcee',
        'gestion_auto',
        'utilisation_temp_exterieure',
        'mi_saison'
    ];

    const regex = new RegExp(`^SET_INPUT_TEXT_(${helperNames.join('|')})_(.*)$`);
    const match = command.match(regex);

    if (match) {
        const helperName = match[1];
        let value = match[2];

        if (helperName !== 'utilisation_temp_exterieure') {
            value = value.replace(/_/g, ' ');
        }
        
        const entityId = `input_text.nodered_etat_${helperName}`; 

        msg.payload = {
            type: "call_service",
            domain: "input_text",
            service: "set_value",
            service_data: {
                value: value
            },
            target: {
                entity_id: entityId
            }
        };
        return msg;
    }

    return null;

} catch (e) {
    node.error("Erreur dans le dispatcher input_text : " + e.message, msg);
    return null;
}

Coeur du noeud Dispatcher Voiture

// ### CODE DU NŒUD : DISPATCHER VOITURE - AMÉLIORÉ ###

try {
    const command = msg.payload;
    
    if (!command || typeof command !== 'string') {
        node.warn("Commande invalide reçue par le dispatcher voiture");
        return null;
    }
    
    const regex = /^(TURN_ON|TURN_OFF)_(input_boolean\.[a-zA-Z0-9_]+)$/;
    const match = command.match(regex);

    if (match) {
        const service = match[1].toLowerCase(); // 'turn_on' or 'turn_off'
        const entityId = match[2];

        msg.payload = {
            type: "call_service",
            domain: "input_boolean",
            service: service,
            target: {
                entity_id: entityId
            }
        };
        return msg;
    }

    return null;

} catch (e) {
    node.error("Erreur dans le dispatcher voiture : " + e.message, msg);
    return null;
}

Il vous sera trés facile d’utiliser ce code en tant que base via une IA pour faire les modifications souhaitées.

Code du flux complet:

[
    {
        "id": "236bdcb1284426a5",
        "type": "tab",
        "label": "GEMINI GESTION MAISON",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "30d526b71c5a89fc",
        "type": "group",
        "z": "236bdcb1284426a5",
        "style": {
            "stroke": "#999999",
            "stroke-opacity": "1",
            "fill": "none",
            "fill-opacity": "1",
            "label": true,
            "label-position": "nw",
            "color": "#a4a4a4"
        },
        "nodes": [
            "0462f0690349fd6d",
            "9dbfa8bafbdc3938"
        ],
        "x": 1854,
        "y": 1019,
        "w": 312,
        "h": 142
    },
    {
        "id": "9f231d2df151524f",
        "type": "group",
        "z": "236bdcb1284426a5",
        "style": {
            "stroke": "#999999",
            "stroke-opacity": "1",
            "fill": "none",
            "fill-opacity": "1",
            "label": true,
            "label-position": "nw",
            "color": "#a4a4a4"
        },
        "nodes": [
            "b6e8a65a855cc5ab",
            "cdf300da4292e6e7",
            "7b1641a88e3b768a"
        ],
        "x": 1854,
        "y": 1179,
        "w": 272,
        "h": 202
    },
    {
        "id": "e584f80d1f57904e",
        "type": "group",
        "z": "236bdcb1284426a5",
        "style": {
            "stroke": "#999999",
            "stroke-opacity": "1",
            "fill": "none",
            "fill-opacity": "1",
            "label": true,
            "label-position": "nw",
            "color": "#a4a4a4"
        },
        "nodes": [
            "b075d9b0eea63d81",
            "f5061a132134d437"
        ],
        "x": 1854,
        "y": 1399,
        "w": 212,
        "h": 162
    },
    {
        "id": "2f5e2fcfbe552773",
        "type": "group",
        "z": "236bdcb1284426a5",
        "style": {
            "stroke": "#999999",
            "stroke-opacity": "1",
            "fill": "none",
            "fill-opacity": "1",
            "label": true,
            "label-position": "nw",
            "color": "#a4a4a4"
        },
        "nodes": [
            "4db9e0618b1396b1",
            "36388a7c171e859c",
            "0c2a1314b121cfe2",
            "f7056c1092e92d0b",
            "0600826375f8dd75",
            "f504f8535b3332cd",
            "06bb7e8c739fefe7",
            "bf050c8d7f1f56a9",
            "d4d45c71f888bbb8",
            "6d2550589dc092a8"
        ],
        "x": 1854,
        "y": 1599,
        "w": 332,
        "h": 442
    },
    {
        "id": "911ce4bfeaf3c135",
        "type": "group",
        "z": "236bdcb1284426a5",
        "style": {
            "stroke": "#999999",
            "stroke-opacity": "1",
            "fill": "none",
            "fill-opacity": "1",
            "label": true,
            "label-position": "nw",
            "color": "#a4a4a4"
        },
        "nodes": [
            "23193f1cf1ab4828",
            "3dc9dc2f1ee7e481",
            "567cfc1553529b90",
            "f83056ad321e8412",
            "f082998a84a2efd6",
            "18172beef3583059"
        ],
        "x": 1854,
        "y": 2059,
        "w": 632,
        "h": 222
    },
    {
        "id": "60a43b19dc469992",
        "type": "group",
        "z": "236bdcb1284426a5",
        "style": {
            "stroke": "#999999",
            "stroke-opacity": "1",
            "fill": "none",
            "fill-opacity": "1",
            "label": true,
            "label-position": "nw",
            "color": "#a4a4a4"
        },
        "nodes": [
            "381d7a4acb41d4f1",
            "0440734ffb117e0b",
            "3888edd9a1921ee2",
            "6ef2e69c91c99492",
            "9a482a2eba7531d1",
            "1731f716abe1bf2d",
            "792c83c59b62bdfc",
            "701e199ff346c6ac",
            "c0e73fed01db4c82",
            "dfcb40e00213e5a7",
            "f138917af391766c",
            "aad154b7d41f8ef1",
            "ca2320cc9cfc5f28",
            "bc5f6b2053564699",
            "3a6c83d3dfce5c85"
        ],
        "x": 2334,
        "y": 459,
        "w": 712,
        "h": 322
    },
    {
        "id": "fcc50eded02e8fc9",
        "type": "group",
        "z": "236bdcb1284426a5",
        "style": {
            "stroke": "#999999",
            "stroke-opacity": "1",
            "fill": "none",
            "fill-opacity": "1",
            "label": true,
            "label-position": "nw",
            "color": "#a4a4a4"
        },
        "nodes": [
            "177b6a4071aa1684",
            "14694fc768b31de9",
            "1adc54b641b1403b",
            "7574af6c1288c89c",
            "43cb5744b2100367",
            "aa0f7df911da2d81",
            "36e1491f3c52a3f0"
        ],
        "x": 1494,
        "y": 79,
        "w": 612,
        "h": 322
    },
    {
        "id": "02df0b2dbe863781",
        "type": "group",
        "z": "236bdcb1284426a5",
        "style": {
            "stroke": "#999999",
            "stroke-opacity": "1",
            "fill": "none",
            "fill-opacity": "1",
            "label": true,
            "label-position": "nw",
            "color": "#a4a4a4"
        },
        "nodes": [
            "8131d955371456a6",
            "893895549972cd61",
            "87514397dca3f3be",
            "0310f42df5aa96be",
            "1b1d799e4d122d72"
        ],
        "x": 414,
        "y": 359,
        "w": 332,
        "h": 322
    },
    {
        "id": "babe010276d2c37d",
        "type": "group",
        "z": "236bdcb1284426a5",
        "style": {
            "stroke": "#999999",
            "stroke-opacity": "1",
            "fill": "none",
            "fill-opacity": "1",
            "label": true,
            "label-position": "nw",
            "color": "#a4a4a4"
        },
        "nodes": [
            "216b5ec1cb5975f0",
            "773615832d306ad7",
            "8aa1c4b133e389fe",
            "2496a518e5e0c785",
            "e386284bf0cbfb88",
            "43b9f87012cfcd5a",
            "37cb0830a57eb629"
        ],
        "x": 394,
        "y": 779,
        "w": 352,
        "h": 462
    },
    {
        "id": "4c983f0a668e4a0a",
        "type": "group",
        "z": "236bdcb1284426a5",
        "style": {
            "stroke": "#999999",
            "stroke-opacity": "1",
            "fill": "none",
            "fill-opacity": "1",
            "label": true,
            "label-position": "nw",
            "color": "#a4a4a4"
        },
        "nodes": [
            "a9b8c7d6.e5f4g3",
            "e4a2a1b9.a5d3f",
            "b90c93de46a3ed3b",
            "7912c6025ff824ef",
            "2ba022c50fc1e42d",
            "0b08f44d3510a7fe",
            "e91a1b1b.16e5e8",
            "601203dc5389b7f7",
            "17651a22e07cec93"
        ],
        "x": -26,
        "y": -21,
        "w": 1272,
        "h": 202
    },
    {
        "id": "0ae09618a71e329f",
        "type": "group",
        "z": "236bdcb1284426a5",
        "style": {
            "stroke": "#999999",
            "stroke-opacity": "1",
            "fill": "none",
            "fill-opacity": "1",
            "label": true,
            "label-position": "nw",
            "color": "#a4a4a4"
        },
        "nodes": [
            "12a9b25981eba61a",
            "1519d53bfdfd6107",
            "13080791b21a5c39"
        ],
        "x": 1854,
        "y": 816.5,
        "w": 472,
        "h": 164.5
    },
    {
        "id": "a9b8c7d6.e5f4g3",
        "type": "inject",
        "z": "236bdcb1284426a5",
        "g": "4c983f0a668e4a0a",
        "name": "Bouton de Test",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 140,
        "y": 80,
        "wires": [
            [
                "b90c93de46a3ed3b"
            ]
        ]
    },
    {
        "id": "e4a2a1b9.a5d3f",
        "type": "cronplus",
        "z": "236bdcb1284426a5",
        "g": "4c983f0a668e4a0a",
        "name": "Déclencheur Horaire",
        "outputField": "payload",
        "timeZone": "Europe/Paris",
        "storeName": "default",
        "commandResponseMsgOutput": "output1",
        "defaultLocation": "",
        "defaultLocationType": "default",
        "outputs": 1,
        "options": [
            {
                "name": "schedule",
                "topic": "schedule",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "30 0 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule9",
                "topic": "schedule9",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "0 02 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule10",
                "topic": "schedule10",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "0 05 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule2",
                "topic": "schedule2",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "0 6 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule3",
                "topic": "schedule3",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "0 8 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule4",
                "topic": "schedule4",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "30 12 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule5",
                "topic": "schedule5",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "0 14 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule6",
                "topic": "schedule6",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "0 18 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule7",
                "topic": "schedule7",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "0 20 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule8",
                "topic": "schedule8",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "0 22 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule11",
                "topic": "schedule9",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "0 23 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule12",
                "topic": "topic12",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "0 16 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule13",
                "topic": "topic13",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "30 11 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule14",
                "topic": "topic14",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "30 13 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule15",
                "topic": "topic15",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "30 17 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule16",
                "topic": "topic16",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "0 10 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule17",
                "topic": "topic17",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "0 7 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule18",
                "topic": "topic18",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "0 9 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule19",
                "topic": "topic19",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "0 01 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            },
            {
                "name": "schedule20",
                "topic": "topic20",
                "payloadType": "default",
                "payload": "",
                "expressionType": "cron",
                "expression": "0 07 * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            }
        ],
        "x": 120,
        "y": 120,
        "wires": [
            [
                "b90c93de46a3ed3b"
            ]
        ]
    },
    {
        "id": "e91a1b1b.16e5e8",
        "type": "function",
        "z": "236bdcb1284426a5",
        "g": "4c983f0a668e4a0a",
        "name": "2. Définir les Variables de Contexte",
        "func": "// ============================================================\n// NŒUD : VARIABLES DE CONTEXTE - VERSION V8 NOVALIX\n// Type: Function\n// ============================================================\n// Ce nœud récupère tous les états de Home Assistant et construit\n// le payload qui sera utilisé par les nœuds de logique.\n// \n// ✅ NOUVEAUTÉS V8 :\n// - Ajout des 3 helpers Novalix\n// - Calcul des plages horaires HC Novalix\n// - Support double fournisseur (Tempo + Novalix)\n// ============================================================\n\n// Helper pour trouver une entité dans le tableau et retourner son état\nfunction getState(entities, entityId) {\n    const entity = entities.find(e => e.entity_id === entityId);\n    if (!entity) {\n        node.warn(`Entité non trouvée : ${entityId}`);\n        return null;\n    }\n    if (entityId.startsWith('input_number') || entityId.startsWith('sensor')) {\n        const num = parseFloat(entity.state);\n        return isNaN(num) ? entity.state : num;\n    }\n    if (entityId.startsWith('input_boolean') || entityId.startsWith('binary_sensor')) {\n        return entity.state === 'on';\n    }\n    if (entityId.startsWith('input_select')) {\n        return entity.state;\n    }\n    return entity.state;\n}\n\n// Helper pour obtenir un attribut spécifique d'une entité\nfunction getAttribute(entities, entityId, attribute) {\n    const entity = entities.find(e => e.entity_id === entityId);\n    if (!entity || !entity.attributes || typeof entity.attributes[attribute] === 'undefined') {\n        node.warn(`Attribut '${attribute}' non trouvé pour l'entité : ${entityId}`);\n        return null;\n    }\n    return entity.attributes[attribute];\n}\n\nconst now = new Date();\nconst currentMonth = now.getMonth(); // 0 for January, 11 for December\nconst currentDay = now.getDay(); // 0 = Dimanche, 1 = Lundi, ..., 6 = Samedi\n\n// --- 1. Récupérer les données météo du message précédent (VALIDATION MÉTÉO) ---\nconst forecastData = msg.forecast_data;\n\n// --- 2. Récupérer les états de toutes les entités Home Assistant ---\nconst entityIds = [\n    'sensor.rte_tempo_couleur_actuelle',\n    'sensor.rte_tempo_prochaine_couleur',\n    'sensor.date_time',\n    'weather.vergeze',\n    'sensor.presence_etat',\n    'alarm_control_panel.alarmo',\n    'sensor.presence_personnes',\n    'sensor.sofar_battery_soc',\n    'sensor.sofar_grid_power',\n    'sensor.sofar_solar_pv_generation',\n    'sensor.volvo_xc40_batterie',\n    'input_number.temp_confort_hiver',\n    'input_number.temp_eco_nuit_hiver',\n    'input_number.temp_confort_ete',\n    'input_number.temp_eco_nuit_ete',\n    'input_number.temp_hors_gel',\n    'input_number.temp_eco_jour_rouge',\n    'input_boolean.activation_prechauffage_matin',\n    'input_number.heure_debut_prechauffage_matin',\n    'input_number.heure_fin_prechauffage_matin',\n    'input_boolean.activation_prechauffage_soir',\n    'input_number.heure_debut_prechauffage_soir',\n    'input_number.heure_fin_prechauffage_soir',\n    'input_boolean.gestion_auto_bureau',\n    'input_boolean.gestion_auto_salon',\n    'input_boolean.gestion_auto_ch_parents',\n    'input_boolean.gestion_auto_ch_lea',\n    'input_boolean.gestion_auto_ch_celia',\n    'input_boolean.ventilation_auto_mi_saison',\n    'input_number.heure_debut_ventilation_mi_saison',\n    'input_number.heure_fin_ventilation_mi_saison',\n    'input_number.ajustement_temp_dynamique',\n    'input_number.seuil_modulation_eco',\n    'input_number.seuil_boost_solaire',\n    'input_number.seuil_froid_exterieur',\n    'input_number.seuil_chaud_exterieur',\n    'input_number.seuil_batterie_auto_solaire_nuit',\n    'climate.bureau',\n    'climate.salon',\n    'climate.parents',\n    'climate.lea',\n    'climate.celia',\n    'input_number.duree_forcage_manuel',\n    'input_text.override_timestamp_bureau',\n    'input_text.override_timestamp_salon',\n    'input_text.override_timestamp_chambre_parents',\n    'input_text.override_timestamp_chambre_lea',\n    'input_text.override_timestamp_chambre_celia',\n    'input_number.last_temp_set_bureau',\n    'input_number.last_temp_set_salon',\n    'input_number.last_temp_set_chambre_parents',\n    'input_number.last_temp_set_chambre_lea',\n    'input_number.last_temp_set_chambre_celia',\n    'input_boolean.gestion_automatique_batterie',\n    'input_select.batterie_maison_mode_force',\n    'input_boolean.voiture_charge_forcee',\n    'input_number.voiture_consigne_charge',\n    'input_number.seuil_surplus_solaire_voiture',\n    'input_boolean.rapports_maison',\n    'input_number.heure_rapport_quotidien',\n    'input_number.minute_rapport_quotidien',\n    'input_boolean.gestion_dynamique_conso_elec',\n    'input_boolean.gestion_dynamique_temp_exterieure',\n    'input_number.seuil_temp_max_hiver',\n    'input_number.seuil_temp_max_ete',\n    'input_number.hysteresis_saison',\n    // Helpers de gestion différenciée\n    'input_boolean.gestion_presence_differentielle',\n    'input_number.heure_debut_pause_dejeuner',\n    'input_number.minute_debut_pause_dejeuner',\n    'input_number.heure_fin_pause_dejeuner',\n    'input_number.minute_fin_pause_dejeuner',\n    'input_number.heure_retour_mode_normal',\n    'input_number.minute_retour_mode_normal',\n    'input_select.jours_gestion_differentielle',\n    'input_boolean.gestion_diff_lundi',\n    'input_boolean.gestion_diff_mardi',\n    'input_boolean.gestion_diff_mercredi',\n    'input_boolean.gestion_diff_jeudi',\n    'input_boolean.gestion_diff_vendredi',\n    'input_boolean.gestion_diff_samedi',\n    'input_boolean.gestion_diff_dimanche',\n    'input_boolean.suivi_presence_yohan',\n    'input_boolean.suivi_presence_jenny',\n    'input_boolean.suivi_presence_invite',\n    // ✅ NOUVEAUX HELPERS V8 NOVALIX\n    'input_select.mode_fournisseur_electrique',\n    'input_number.novalix_seuil_recharge_aprem',\n    'input_number.novalix_surconfort_hc'\n];\n\nconst allStates = global.get('homeassistant.homeAssistant.states');\n\nif (!allStates) {\n    node.error(\"Le contexte global de Home Assistant n'est pas disponible. Vérifiez la connexion de l'intégration Node-RED.\", msg);\n    return null;\n}\n\nconst haEntities = [];\nentityIds.forEach(entityId => {\n    if (allStates[entityId]) {\n        haEntities.push(allStates[entityId]);\n    } else {\n        node.warn(`L'entité \"${entityId}\" n'a pas été trouvée dans le contexte global de Home Assistant.`);\n    }\n});\n\nif (haEntities.length === 0) {\n    node.error(\"Aucune des entités spécifiées n'a été trouvée. Le flux s'arrête.\", msg);\n    return null;\n}\n\n// --- Logique de présence ---\nconst presenceEtatHA = getState(haEntities, 'sensor.presence_etat') || 'ABSENT';\nconst presencePersonnesHA = getState(haEntities, 'sensor.presence_personnes') || 'Personne';\nconst alarmeState = getState(haEntities, 'alarm_control_panel.alarmo') || 'disarmed';\nconst alarmeActivee = alarmeState !== 'disarmed';\n\nconst suiviYohan = getState(haEntities, 'input_boolean.suivi_presence_yohan') !== false;\nconst suiviJenny = getState(haEntities, 'input_boolean.suivi_presence_jenny') !== false;\nconst suiviInvite = getState(haEntities, 'input_boolean.suivi_presence_invite') !== false;\n\nlet personnesDetectees = [];\nif (presencePersonnesHA && presencePersonnesHA !== 'Personne') {\n    personnesDetectees = presencePersonnesHA.split(', ').map(name => name.trim());\n}\n\nlet personnesPresentes = [];\npersonnesDetectees.forEach(personne => {\n    if (personne === 'YOHAN' && suiviYohan) personnesPresentes.push('YOHAN');\n    else if (personne === 'JENNY' && suiviJenny) personnesPresentes.push('JENNY');\n    else if (personne === 'INVITE' && suiviInvite) personnesPresentes.push('INVITE');\n});\n\nconst maisonInhabitee = (personnesPresentes.length === 0) || alarmeActivee;\n\nlet profilPresence = 'ABSENT';\nconst isYohanPresent = personnesPresentes.includes('YOHAN');\nconst isJennyPresente = personnesPresentes.includes('JENNY');\nconst isInvitePresent = personnesPresentes.includes('INVITE');\n\nif (maisonInhabitee) {\n    profilPresence = 'ABSENT';\n} else if (isYohanPresent && isJennyPresente) {\n    profilPresence = 'COUPLE';\n} else if (isYohanPresent && !isJennyPresente && !isInvitePresent) {\n    profilPresence = 'YOHAN_SEUL';\n} else if (isJennyPresente && !isYohanPresent && !isInvitePresent) {\n    profilPresence = 'JENNY_SEULE';\n} else if (isInvitePresent && !isYohanPresent && !isJennyPresente) {\n    profilPresence = 'INVITE_SEUL';\n} else {\n    profilPresence = 'MIXTE';\n}\n\n// --- Calcul du surplus de puissance ---\nconst puissanceReseau = getState(haEntities, 'sensor.sofar_grid_power') || 0;\nconst surplusPuissance = (puissanceReseau < 0) ? -puissanceReseau : 0;\n\n// --- Logique Météo du jour ---\nconst meteoJourState = getState(haEntities, 'weather.vergeze');\nconst bonneMeteoStates = ['sunny', 'partlycloudy', 'clear-night'];\nconst meteoJour = bonneMeteoStates.includes(meteoJourState) ? 'BONNE' : 'MAUVAISE';\n\n// --- Gestion différenciée ---\nconst gestionPresenceDifferentielle = getState(haEntities, 'input_boolean.gestion_presence_differentielle') || false;\nconst heureDebutPauseDejeuner = getState(haEntities, 'input_number.heure_debut_pause_dejeuner') || 12;\nconst minuteDebutPauseDejeuner = getState(haEntities, 'input_number.minute_debut_pause_dejeuner') || 0;\nconst heureFinPauseDejeuner = getState(haEntities, 'input_number.heure_fin_pause_dejeuner') || 13;\nconst minuteFinPauseDejeuner = getState(haEntities, 'input_number.minute_fin_pause_dejeuner') || 30;\nconst heureRetourModeNormal = getState(haEntities, 'input_number.heure_retour_mode_normal') || 17;\nconst minuteRetourModeNormal = getState(haEntities, 'input_number.minute_retour_mode_normal') || 30;\nconst joursGestionDifferentielle = getState(haEntities, 'input_select.jours_gestion_differentielle') || 'Tous les jours';\n\nconst gestionDiffLundi = getState(haEntities, 'input_boolean.gestion_diff_lundi') !== false;\nconst gestionDiffMardi = getState(haEntities, 'input_boolean.gestion_diff_mardi') !== false;\nconst gestionDiffMercredi = getState(haEntities, 'input_boolean.gestion_diff_mercredi') !== false;\nconst gestionDiffJeudi = getState(haEntities, 'input_boolean.gestion_diff_jeudi') !== false;\nconst gestionDiffVendredi = getState(haEntities, 'input_boolean.gestion_diff_vendredi') !== false;\nconst gestionDiffSamedi = getState(haEntities, 'input_boolean.gestion_diff_samedi') !== false;\nconst gestionDiffDimanche = getState(haEntities, 'input_boolean.gestion_diff_dimanche') !== false;\n\nlet gestionDiffActiveAujourdhui = false;\nif (gestionPresenceDifferentielle) {\n    if (joursGestionDifferentielle === 'Tous les jours') {\n        gestionDiffActiveAujourdhui = true;\n    } else if (joursGestionDifferentielle === 'Lundi au Vendredi') {\n        gestionDiffActiveAujourdhui = (currentDay >= 1 && currentDay <= 5);\n    } else if (joursGestionDifferentielle === 'Lundi au Samedi') {\n        gestionDiffActiveAujourdhui = (currentDay >= 1 && currentDay <= 6);\n    } else if (joursGestionDifferentielle === 'Personnalisé') {\n        const joursBooleans = [\n            gestionDiffDimanche, gestionDiffLundi, gestionDiffMardi, gestionDiffMercredi,\n            gestionDiffJeudi, gestionDiffVendredi, gestionDiffSamedi\n        ];\n        gestionDiffActiveAujourdhui = joursBooleans[currentDay];\n    }\n}\n\n// ✅ NOUVEAUTÉS V8 : Récupération des helpers Novalix\nconst modeFournisseur = getState(haEntities, 'input_select.mode_fournisseur_electrique') || 'Tempo';\nconst novalixSeuilRechargeAprem = getState(haEntities, 'input_number.novalix_seuil_recharge_aprem') || 50;\nconst novalixSurconfortHC = getState(haEntities, 'input_number.novalix_surconfort_hc') || 1.0;\n\n// ✅ CALCUL DES PLAGES HORAIRES NOVALIX\nconst heureActuelle = now.getHours();\nconst minuteActuelle = now.getMinutes();\nconst minutesTotales = heureActuelle * 60 + minuteActuelle;\n\n// HC Novalix nuit : 01h00-07h00\nconst isHCNovalixNuit = (heureActuelle >= 1 && heureActuelle < 7);\n\n// HC Novalix après-midi : 14h30-16h30\nconst minutesDebutHCAprem = 14 * 60 + 30; // 870 minutes\nconst minutesFinHCAprem = 16 * 60 + 30;   // 990 minutes\nconst isHCNovalixAprem = (minutesTotales >= minutesDebutHCAprem && minutesTotales < minutesFinHCAprem);\n\n// --- Création du payload ---\nconst newPayload = {\n    COULEUR_TEMPO: getState(haEntities, 'sensor.rte_tempo_couleur_actuelle'),\n    COULEUR_TEMPO_PROCHAINE: getState(haEntities, 'sensor.rte_tempo_prochaine_couleur'),\n    HEURE: heureActuelle,\n    MINUTE: minuteActuelle,\n    METEO_JOUR: meteoJour,\n    MAISON_INHABITEE: maisonInhabitee,\n    NIVEAU_BATTERIE_MAISON: getState(haEntities, 'sensor.sofar_battery_soc'),\n    SURPLUS_PUISSANCE: surplusPuissance,\n    PUISSANCE_SOLAIRE_ACTUELLE: getState(haEntities, 'sensor.sofar_solar_pv_generation'),\n    PUISSANCE_RESEAU: puissanceReseau,\n    TEMP_EXTERIEURE_ACTUELLE: getAttribute(haEntities, 'weather.vergeze', 'temperature'),\n    PERSONNES_PRESENTES: personnesPresentes,\n    PROFIL_PRESENCE: profilPresence,\n    IS_YOHAN_PRESENT: isYohanPresent,\n    IS_JENNY_PRESENTE: isJennyPresente,\n    IS_INVITE_PRESENT: isInvitePresent,\n    NIVEAU_BATTERIE_VOITURE: getState(haEntities, 'sensor.volvo_xc40_batterie') || -1,\n\n    // Helpers de température\n    TEMP_CONFORT_HIVER: getState(haEntities, 'input_number.temp_confort_hiver'),\n    TEMP_ECO_NUIT_HIVER: getState(haEntities, 'input_number.temp_eco_nuit_hiver'),\n    TEMP_CONFORT_ETE: getState(haEntities, 'input_number.temp_confort_ete'),\n    TEMP_ECO_NUIT_ETE: getState(haEntities, 'input_number.temp_eco_nuit_ete'),\n    TEMP_HORS_GEL: getState(haEntities, 'input_number.temp_hors_gel'),\n    TEMP_ECO_JOUR_ROUGE: getState(haEntities, 'input_number.temp_eco_jour_rouge'),\n\n    // Helpers d'horaires\n    ACTIVATION_PRECHAUFFAGE_MATIN: getState(haEntities, 'input_boolean.activation_prechauffage_matin'),\n    HEURE_DEBUT_PRECHAUFFAGE_MATIN: getState(haEntities, 'input_number.heure_debut_prechauffage_matin'),\n    HEURE_FIN_PRECHAUFFAGE_MATIN: getState(haEntities, 'input_number.heure_fin_prechauffage_matin'),\n    ACTIVATION_PRECHAUFFAGE_SOIR: getState(haEntities, 'input_boolean.activation_prechauffage_soir'),\n    HEURE_DEBUT_PRECHAUFFAGE_SOIR: getState(haEntities, 'input_number.heure_debut_prechauffage_soir'),\n    HEURE_FIN_PRECHAUFFAGE_SOIR: getState(haEntities, 'input_number.heure_fin_prechauffage_soir'),\n    VENTILATION_AUTO_MI_SAISON: getState(haEntities, 'input_boolean.ventilation_auto_mi_saison'),\n    HEURE_DEBUT_VENTILATION: getState(haEntities, 'input_number.heure_debut_ventilation_mi_saison'),\n    HEURE_FIN_VENTILATION: getState(haEntities, 'input_number.heure_fin_ventilation_mi_saison'),\n\n    // Helpers de gestion dynamique\n    AJUSTEMENT_TEMP_DYNAMIQUE: getState(haEntities, 'input_number.ajustement_temp_dynamique'),\n    SEUIL_MODULATION_ECO: getState(haEntities, 'input_number.seuil_modulation_eco'),\n    SEUIL_BOOST_SOLAIRE: getState(haEntities, 'input_number.seuil_boost_solaire'),\n    SEUIL_FROID_EXTERIEUR: getState(haEntities, 'input_number.seuil_froid_exterieur'),\n    SEUIL_CHAUD_EXTERIEUR: getState(haEntities, 'input_number.seuil_chaud_exterieur'),\n    GESTION_DYNAMIQUE_CONSO_ELEC: getState(haEntities, 'input_boolean.gestion_dynamique_conso_elec'),\n    GESTION_DYNAMIQUE_TEMP_EXT: getState(haEntities, 'input_boolean.gestion_dynamique_temp_exterieure'),\n    SEUIL_TEMP_MAX_HIVER: getState(haEntities, 'input_number.seuil_temp_max_hiver'),\n    SEUIL_TEMP_MAX_ETE: getState(haEntities, 'input_number.seuil_temp_max_ete'),\n    HYSTERESIS_SAISON: getState(haEntities, 'input_number.hysteresis_saison'),\n\n    // Helpers de gestion auto\n    GESTION_AUTO_BUREAU: getState(haEntities, 'input_boolean.gestion_auto_bureau'),\n    GESTION_AUTO_SALON: getState(haEntities, 'input_boolean.gestion_auto_salon'),\n    GESTION_AUTO_CH_PARENTS: getState(haEntities, 'input_boolean.gestion_auto_ch_parents'),\n    GESTION_AUTO_CH_LEA: getState(haEntities, 'input_boolean.gestion_auto_ch_lea'),\n    GESTION_AUTO_CH_CELIA: getState(haEntities, 'input_boolean.gestion_auto_ch_celia'),\n\n    // Helpers de forçage\n    DUREE_FORCAGE_MANUEL: getState(haEntities, 'input_number.duree_forcage_manuel'),\n    VOITURE_CHARGE_FORCEE: getState(haEntities, 'input_boolean.voiture_charge_forcee'),\n    CONSIGNE_CHARGE_VOITURE: getState(haEntities, 'input_number.voiture_consigne_charge'),\n    SEUIL_SURPLUS_SOLAIRE_VOITURE: getState(haEntities, 'input_number.seuil_surplus_solaire_voiture'),\n\n    // Helpers Batterie\n    GESTION_AUTO_BATTERIE: getState(haEntities, 'input_boolean.gestion_automatique_batterie'),\n    MODE_FORCE_BATTERIE_SELECT: getState(haEntities, 'input_select.batterie_maison_mode_force'),\n    SEUIL_BATTERIE_AUTO_SOLAIRE_NUIT: getState(haEntities, 'input_number.seuil_batterie_auto_solaire_nuit'),\n\n    // Helpers de gestion différenciée\n    GESTION_PRESENCE_DIFFERENTIELLE: gestionPresenceDifferentielle,\n    GESTION_DIFF_ACTIVE_AUJOURDHUI: gestionDiffActiveAujourdhui,\n    HEURE_DEBUT_PAUSE_DEJEUNER: heureDebutPauseDejeuner,\n    MINUTE_DEBUT_PAUSE_DEJEUNER: minuteDebutPauseDejeuner,\n    HEURE_FIN_PAUSE_DEJEUNER: heureFinPauseDejeuner,\n    MINUTE_FIN_PAUSE_DEJEUNER: minuteFinPauseDejeuner,\n    HEURE_RETOUR_MODE_NORMAL: heureRetourModeNormal,\n    MINUTE_RETOUR_MODE_NORMAL: minuteRetourModeNormal,\n\n    // ✅ NOUVEAUX HELPERS V8 NOVALIX\n    MODE_FOURNISSEUR: modeFournisseur,\n    NOVALIX_SEUIL_RECHARGE_APREM: novalixSeuilRechargeAprem,\n    NOVALIX_SURCONFORT_HC: novalixSurconfortHC,\n    IS_HC_NOVALIX_NUIT: isHCNovalixNuit,\n    IS_HC_NOVALIX_APREM: isHCNovalixAprem,\n\n    // Objets complexes\n    CONSIGNES_ACTUELLES: {\n        BUREAU: getAttribute(haEntities, 'climate.bureau', 'temperature'),\n        SALON: getAttribute(haEntities, 'climate.salon', 'temperature'),\n        CHAMBRE_PARENTS: getAttribute(haEntities, 'climate.parents', 'temperature'),\n        CHAMBRE_LEA: getAttribute(haEntities, 'climate.lea', 'temperature'),\n        CHAMBRE_CELIA: getAttribute(haEntities, 'climate.celia', 'temperature'),\n    },\n    ETATS_CLIM: {\n        BUREAU: getState(haEntities, 'climate.bureau'),\n        SALON: getState(haEntities, 'climate.salon'),\n        CHAMBRE_PARENTS: getState(haEntities, 'climate.parents'),\n        CHAMBRE_LEA: getState(haEntities, 'climate.lea'),\n        CHAMBRE_CELIA: getState(haEntities, 'climate.celia'),\n    },\n    OVERRIDE_TIMESTAMPS: {\n        BUREAU: getState(haEntities, 'input_text.override_timestamp_bureau'),\n        SALON: getState(haEntities, 'input_text.override_timestamp_salon'),\n        CHAMBRE_PARENTS: getState(haEntities, 'input_text.override_timestamp_chambre_parents'),\n        CHAMBRE_LEA: getState(haEntities, 'input_text.override_timestamp_chambre_lea'),\n        CHAMBRE_CELIA: getState(haEntities, 'input_text.override_timestamp_chambre_celia'),\n    },\n    LAST_TEMP_SET: {\n        BUREAU: getState(haEntities, 'input_number.last_temp_set_bureau'),\n        SALON: getState(haEntities, 'input_number.last_temp_set_salon'),\n        CHAMBRE_PARENTS: getState(haEntities, 'input_number.last_temp_set_chambre_parents'),\n        CHAMBRE_LEA: getState(haEntities, 'input_number.last_temp_set_chambre_lea'),\n        CHAMBRE_CELIA: getState(haEntities, 'input_number.last_temp_set_chambre_celia'),\n    },\n    RAPPORT_DEBUG_MODE: getState(haEntities, 'input_boolean.rapports_maison'),\n    HEURE_RAPPORT_QUOTIDIEN: getState(haEntities, 'input_number.heure_rapport_quotidien'),\n    MINUTE_RAPPORT_QUOTIDIEN: getState(haEntities, 'input_number.minute_rapport_quotidien')\n};\n\n// Logique jour rouge (uniquement si mode Tempo)\nnewPayload.IS_NIGHT_BEFORE_RED_DAY = false;\nif (modeFournisseur === 'Tempo') {\n    newPayload.IS_NIGHT_BEFORE_RED_DAY = (\n        newPayload.HEURE >= 22 && \n        newPayload.COULEUR_TEMPO_PROCHAINE === 'Rouge' && \n        newPayload.COULEUR_TEMPO !== 'Rouge'\n    );\n}\n\n// Intégration des données météo\nif (forecastData && forecastData.forecast && forecastData.forecast.length > 0) {\n    const bonneMeteoStatesForecast = ['sunny', 'partlycloudy', 'clear-night'];\n    newPayload.METEO_DEMAIN = bonneMeteoStatesForecast.includes(forecastData.forecast[0].condition) ? 'BONNE' : 'MAUVAISE';\n    newPayload.TEMP_MAX_PREVUE = forecastData.forecast[0].temperature;\n} else {\n    newPayload.METEO_DEMAIN = 'INCONNUE';\n    newPayload.TEMP_MAX_PREVUE = null;\n}\n\n// --- LOGIQUE DE DÉTECTION SAISONNIÈRE ---\nlet calculatedSeason = flow.get('calculated_season');\nconst tempExt = newPayload.TEMP_EXTERIEURE_ACTUELLE;\nconst seuilHiver = newPayload.SEUIL_TEMP_MAX_HIVER;\nconst seuilEte = newPayload.SEUIL_TEMP_MAX_ETE;\nconst hysteresis = newPayload.HYSTERESIS_SAISON;\n\nconst moisHiver = [11, 0, 1];\nconst moisMiSaisonHiver = [2, 3, 10];\nconst moisMiSaisonEte = [4, 8, 9];\nconst moisEte = [5, 6, 7];\n\nlet tempBasedSeason;\n\nif (calculatedSeason === undefined || calculatedSeason === null) {\n    if (tempExt <= seuilHiver) {\n        tempBasedSeason = 'HIVER';\n    } else if (tempExt >= seuilEte) {\n        tempBasedSeason = 'ÉTÉ';\n    } else if (tempExt < (seuilHiver + seuilEte) / 2) {\n        tempBasedSeason = 'MI-SAISON-HIVER';\n    } else {\n        tempBasedSeason = 'MI-SAISON-ETE';\n    }\n} else {\n    if (calculatedSeason === 'HIVER' && tempExt > seuilHiver + hysteresis) {\n        tempBasedSeason = 'MI-SAISON-HIVER';\n    } else if (calculatedSeason === 'MI-SAISON-HIVER' && tempExt < seuilHiver - hysteresis) {\n        tempBasedSeason = 'HIVER';\n    } else if (calculatedSeason === 'MI-SAISON-HIVER' && tempExt > seuilEte - hysteresis) {\n        tempBasedSeason = 'MI-SAISON-ETE';\n    } else if (calculatedSeason === 'MI-SAISON-ETE' && tempExt < seuilHiver + hysteresis) {\n        tempBasedSeason = 'MI-SAISON-HIVER';\n    } else if (calculatedSeason === 'MI-SAISON-ETE' && tempExt > seuilEte - hysteresis) {\n        tempBasedSeason = 'ÉTÉ';\n    } else if (calculatedSeason === 'ÉTÉ' && tempExt < seuilEte - hysteresis) {\n        tempBasedSeason = 'MI-SAISON-ETE';\n    } else {\n        tempBasedSeason = calculatedSeason;\n    }\n}\n\nlet monthBasedSeason;\nif (moisHiver.includes(currentMonth)) {\n    monthBasedSeason = 'HIVER';\n} else if (moisMiSaisonHiver.includes(currentMonth)) {\n    monthBasedSeason = 'MI-SAISON-HIVER';\n} else if (moisMiSaisonEte.includes(currentMonth)) {\n    monthBasedSeason = 'MI-SAISON-ETE';\n} else if (moisEte.includes(currentMonth)) {\n    monthBasedSeason = 'ÉTÉ';\n} else {\n    monthBasedSeason = 'INCONNU';\n}\n\nconst saisonPrioritaire = ['HIVER', 'MI-SAISON-HIVER', 'ÉTÉ', 'MI-SAISON-ETE'].indexOf(tempBasedSeason) <= ['HIVER', 'MI-SAISON-HIVER', 'ÉTÉ', 'MI-SAISON-ETE'].indexOf(monthBasedSeason) ? tempBasedSeason : monthBasedSeason;\n\nif (saisonPrioritaire !== calculatedSeason) {\n    flow.set('calculated_season', saisonPrioritaire);\n}\n\nnewPayload.MODE_SAISONNIER = saisonPrioritaire;\n\n// ============================================================\n// ✅ V8 : GESTION MODE SÉCURITÉ (capteurs critiques manquants)\n// ============================================================\nconst capteursCritiques = [\n    { nom: 'NIVEAU_BATTERIE_MAISON', valeur: newPayload.NIVEAU_BATTERIE_MAISON },\n    { nom: 'PUISSANCE_RESEAU', valeur: newPayload.PUISSANCE_RESEAU },\n    { nom: 'PUISSANCE_SOLAIRE_ACTUELLE', valeur: newPayload.PUISSANCE_SOLAIRE_ACTUELLE },\n    { nom: 'TEMP_EXTERIEURE_ACTUELLE', valeur: newPayload.TEMP_EXTERIEURE_ACTUELLE }\n];\n\nconst capteursManquants = capteursCritiques.filter(c => c.valeur === null || c.valeur === undefined);\n\nif (capteursManquants.length > 0) {\n    newPayload.MODE_SECURITE = true;\n    newPayload.CAPTEURS_MANQUANTS = capteursManquants.map(c => c.nom);\n    node.warn(`⚠️ MODE SÉCURITÉ ACTIVÉ - Capteurs critiques manquants : ${capteursManquants.map(c => c.nom).join(', ')}`);\n} else {\n    newPayload.MODE_SECURITE = false;\n    newPayload.CAPTEURS_MANQUANTS = [];\n}\n\nmsg.payload = newPayload;\nreturn msg;\n",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 760,
        "y": 140,
        "wires": [
            [
                "8131d955371456a6",
                "17651a22e07cec93"
            ]
        ]
    },
    {
        "id": "e8e81d7f.c22d1",
        "type": "split",
        "z": "236bdcb1284426a5",
        "name": "",
        "splt": "",
        "spltType": "str",
        "arraySplt": "1",
        "arraySpltType": "len",
        "stream": false,
        "addname": "",
        "property": "payload",
        "x": 1130,
        "y": 280,
        "wires": [
            [
                "cb766a8c136712c2"
            ]
        ]
    },
    {
        "id": "c0b22a0a.a2f648",
        "type": "switch",
        "z": "236bdcb1284426a5",
        "name": "4. Aiguillage vers les Commandes",
        "property": "payload",
        "propertyType": "msg",
        "rules": [
            {
                "t": "eq",
                "v": "PACK_BATTERIE_AUTO",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "PACK_BATTERIE_CHARGE",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "PACK_BATTERIE_EQUALIZATION",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "PACK_BATTERIE_DISCHARGE",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "PACK_BATTERIE_STANDBY",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "MODE_AUTO_PISCINE_ON",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "MODE_AUTO_PISCINE_OFF",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "VMC_MODE_NORMAL",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "VMC_MODE_VACANCES",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "CHARGE_VOITURE_ON",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "CHARGE_VOITURE_OFF",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "ECONOMIE_ECONOMIE_ON",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "ECONOMIE_ECONOMIE_OFF",
                "vt": "str"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 13,
        "x": 1640,
        "y": 620,
        "wires": [
            [
                "6ef2e69c91c99492"
            ],
            [
                "0440734ffb117e0b"
            ],
            [
                "ca2320cc9cfc5f28"
            ],
            [
                "792c83c59b62bdfc"
            ],
            [
                "701e199ff346c6ac"
            ],
            [
                "0462f0690349fd6d"
            ],
            [
                "9dbfa8bafbdc3938"
            ],
            [
                "b6e8a65a855cc5ab"
            ],
            [
                "7b1641a88e3b768a"
            ],
            [
                "b075d9b0eea63d81"
            ],
            [
                "f5061a132134d437"
            ],
            [
                "4db9e0618b1396b1",
                "f7056c1092e92d0b",
                "0c2a1314b121cfe2",
                "0600826375f8dd75",
                "d4d45c71f888bbb8",
                "6d2550589dc092a8",
                "f504f8535b3332cd",
                "06bb7e8c739fefe7",
                "bf050c8d7f1f56a9"
            ],
            [
                "23193f1cf1ab4828",
                "f83056ad321e8412",
                "567cfc1553529b90"
            ]
        ]
    },
    {
        "id": "381d7a4acb41d4f1",
        "type": "mqtt out",
        "z": "236bdcb1284426a5",
        "g": "60a43b19dc469992",
        "name": "INVERTER_CHARGE_3000",
        "topic": "Sofar2mqtt/set/charge",
        "qos": "",
        "retain": "",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "3348075e0f83efab",
        "x": 2740,
        "y": 560,
        "wires": []
    },
    {
        "id": "0440734ffb117e0b",
        "type": "change",
        "z": "236bdcb1284426a5",
        "g": "60a43b19dc469992",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "3000",
                "tot": "str"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 2450,
        "y": 560,
        "wires": [
            [
                "381d7a4acb41d4f1"
            ]
        ]
    },
    {
        "id": "3888edd9a1921ee2",
        "type": "mqtt out",
        "z": "236bdcb1284426a5",
        "g": "60a43b19dc469992",
        "name": "INVERTER_AUTO",
        "topic": "Sofar2mqtt/set/auto",
        "qos": "2",
        "retain": "true",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "3348075e0f83efab",
        "x": 2710,
        "y": 500,
        "wires": []
    },
    {
        "id": "6ef2e69c91c99492",
        "type": "change",
        "z": "236bdcb1284426a5",
        "g": "60a43b19dc469992",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "true",
                "tot": "str"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 2450,
        "y": 500,
        "wires": [
            [
                "3888edd9a1921ee2"
            ]
        ]
    },
    {
        "id": "9a482a2eba7531d1",
        "type": "mqtt out",
        "z": "236bdcb1284426a5",
        "g": "60a43b19dc469992",
        "name": "INVERTER_DISCHARGE",
        "topic": "Sofar2mqtt/set/discharge",
        "qos": "",
        "retain": "",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "3348075e0f83efab",
        "x": 2730,
        "y": 680,
        "wires": []
    },
    {
        "id": "1731f716abe1bf2d",
        "type": "mqtt out",
        "z": "236bdcb1284426a5",
        "g": "60a43b19dc469992",
        "name": "INVERTER_STANDBY",
        "topic": "Sofar2mqtt/set/standby",
        "qos": "",
        "retain": "",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "3348075e0f83efab",
        "x": 2720,
        "y": 740,
        "wires": []
    },
    {
        "id": "792c83c59b62bdfc",
        "type": "change",
        "z": "236bdcb1284426a5",
        "g": "60a43b19dc469992",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "1000",
                "tot": "str"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 2450,
        "y": 680,
        "wires": [
            [
                "9a482a2eba7531d1"
            ]
        ]
    },
    {
        "id": "701e199ff346c6ac",
        "type": "change",
        "z": "236bdcb1284426a5",
        "g": "60a43b19dc469992",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "true",
                "tot": "str"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 2450,
        "y": 740,
        "wires": [
            [
                "1731f716abe1bf2d"
            ]
        ]
    },
    {
        "id": "0462f0690349fd6d",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "30d526b71c5a89fc",
        "name": "MODE_AUTO_PISCINE_ON",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_on",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "input_boolean.gestion_piscine"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_on",
        "x": 2000,
        "y": 1060,
        "wires": [
            []
        ]
    },
    {
        "id": "9dbfa8bafbdc3938",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "30d526b71c5a89fc",
        "name": "MODE_AUTO_PISCINE_OFF",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_off",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "input_boolean.gestion_piscine"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_off",
        "x": 2010,
        "y": 1120,
        "wires": [
            []
        ]
    },
    {
        "id": "b6e8a65a855cc5ab",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "9f231d2df151524f",
        "name": "VMC_MODE_NORMAL",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "button.press",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "button.nodered_46e6315a2b659938"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "button",
        "service": "press",
        "x": 1990,
        "y": 1220,
        "wires": [
            []
        ]
    },
    {
        "id": "cdf300da4292e6e7",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "9f231d2df151524f",
        "name": "VMC_MODE_BOOST",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "button.press",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "button.nodered_27029f3265930acc"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "button",
        "service": "press",
        "x": 1980,
        "y": 1280,
        "wires": [
            []
        ]
    },
    {
        "id": "7b1641a88e3b768a",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "9f231d2df151524f",
        "name": "VMC MODE VACANCES",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "button.press",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "button.nodered_afd567bd657196bb"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "button",
        "service": "press",
        "x": 1990,
        "y": 1340,
        "wires": [
            []
        ]
    },
    {
        "id": "b075d9b0eea63d81",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "e584f80d1f57904e",
        "name": "CHARGE_ON",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "button.press",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "button.lektrico_charge_start"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "button",
        "service": "press",
        "x": 1960,
        "y": 1520,
        "wires": [
            []
        ]
    },
    {
        "id": "f5061a132134d437",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "e584f80d1f57904e",
        "name": "CHARGE_OFF",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "button.press",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "button.lektrico_charge_stop"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "button",
        "service": "press",
        "x": 1960,
        "y": 1440,
        "wires": [
            []
        ]
    },
    {
        "id": "c05179d91f2422d0",
        "type": "telegrambot-notify",
        "z": "236bdcb1284426a5",
        "name": "",
        "bot": "514978627925e867",
        "chatId": "-5086065964",
        "message": "",
        "parseMode": "",
        "x": 900,
        "y": 1200,
        "wires": []
    },
    {
        "id": "94560f551005daa5",
        "type": "change",
        "z": "236bdcb1284426a5",
        "name": "",
        "rules": [
            {
                "t": "move",
                "p": "commands",
                "pt": "msg",
                "to": "payload",
                "tot": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 910,
        "y": 580,
        "wires": [
            [
                "e8e81d7f.c22d1"
            ]
        ]
    },
    {
        "id": "177b6a4071aa1684",
        "type": "function",
        "z": "236bdcb1284426a5",
        "g": "fcc50eded02e8fc9",
        "name": "DISPACHEUR DE THERMOSTAT",
        "func": "// ### CODE DU NŒUD : DISPATCHER DE THERMOSTAT - AMÉLIORÉ ###\n\ntry {\n    const command = msg.payload;\n\n    if (!command || typeof command !== 'string') {\n        node.warn(\"Commande invalide reçue par le dispatcher thermostat\");\n        return null;\n    }\n\n    const entityMap = { \n        'BUREAU': 'climate.bureau', \n        'SALON': 'climate.salon', \n        'CHAMBRE_PARENTS': 'climate.parents', \n        'CHAMBRE_LEA': 'climate.lea', \n        'CHAMBRE_CELIA': 'climate.celia' \n    };\n\n    const modeMap = { \n        'CHAUFFAGE': 'heat', \n        'CLIMATISATION': 'cool', \n        'VENTILATION': 'fan_only' \n    };\n\n    let tempRegex = /^(.+)_TEMP_(\\d+\\.?\\d*)$/; \n    let match = command.match(tempRegex);\n\n    if (match) { \n        const piece = match[1]; \n        const temperature = parseFloat(match[2]); \n        const entityId = entityMap[piece]; \n        if (entityId) { \n            msg.payload = { \n                type: \"call_service\", domain: \"climate\", service: \"set_temperature\", \n                service_data: { temperature: temperature }, \n                target: { entity_id: entityId } \n            }; \n            return msg; \n        } \n    }\n\n    let onOffRegex = /^THERMOSTAT_(.+)_(ON|OFF)$/; \n    match = command.match(onOffRegex);\n\n    if (match) { \n        const piece = match[1]; \n        const state = match[2]; \n        const entityId = entityMap[piece]; \n        const service = (state === 'ON') ? 'turn_on' : 'turn_off'; \n        if (entityId) { \n            msg.payload = { \n                type: \"call_service\", domain: \"climate\", service: service, \n                target: { entity_id: entityId } \n            }; \n            return msg; \n        } \n    }\n\n    let modeRegex = /^(.+)_MODE_(CHAUFFAGE|CLIMATISATION|VENTILATION)$/; \n    match = command.match(modeRegex);\n\n    if (match) { \n        const piece = match[1]; \n        const mode = match[2]; \n        const entityId = entityMap[piece]; \n        const hvacMode = modeMap[mode];\n        if (entityId && hvacMode) { \n            msg.payload = { \n                type: \"call_service\", domain: \"climate\", service: \"set_hvac_mode\", \n                service_data: { hvac_mode: hvacMode }, \n                target: { entity_id: entityId } \n            }; \n            return msg; \n        } \n    }\n\n    return null;\n\n} catch (e) {\n    node.error(\"Erreur dans le dispatcher thermostat : \" + e.message, msg);\n    return null;\n}",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1660,
        "y": 120,
        "wires": [
            [
                "14694fc768b31de9",
                "1adc54b641b1403b"
            ]
        ]
    },
    {
        "id": "14694fc768b31de9",
        "type": "ha-api",
        "z": "236bdcb1284426a5",
        "g": "fcc50eded02e8fc9",
        "name": "HA API",
        "server": "bb0a3ead.a33a3",
        "version": 1,
        "debugenabled": false,
        "protocol": "websocket",
        "method": "get",
        "path": "",
        "data": "payload",
        "dataType": "jsonata",
        "responseType": "json",
        "outputProperties": [
            {
                "property": "payload",
                "propertyType": "msg",
                "value": "",
                "valueType": "results"
            }
        ],
        "x": 2020,
        "y": 280,
        "wires": [
            []
        ]
    },
    {
        "id": "1adc54b641b1403b",
        "type": "function",
        "z": "236bdcb1284426a5",
        "g": "fcc50eded02e8fc9",
        "name": "Logger de thermostat",
        "func": "// Ce noeud reçoit le message formaté par le \"Dispatcheur\"\nconst entityId = msg.payload.target.entity_id;\nconst service = msg.payload.service;\nlet logMessage = \"\";\n\n// On adapte le message de log en fonction du service appelé\nif (service === 'set_temperature') {\nconst temperature = msg.payload.service_data.temperature;\nlogMessage = \"[Thermostat] Commande: Régler \" + entityId + \" sur \" + temperature + \"°C\";\n\n} else if (service === 'set_hvac_mode') {\nconst hvacMode = msg.payload.service_data.hvac_mode;\nlogMessage = \"[Thermostat] Commande: Activer mode '\" + hvacMode + \"' sur \" + entityId;\n\n} else { // C'est un service 'turn_on' ou 'turn_off'\nconst action = (service === 'turn_on') ? \"Allumer\" : \"Éteindre\";\nlogMessage = \"[Thermostat] Commande: \" + action + \" \" + entityId;\n}\n\nreturn msg; // MODIFIÉ : Passe le message au nœud suivant",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1660,
        "y": 180,
        "wires": [
            [
                "14694fc768b31de9"
            ]
        ]
    },
    {
        "id": "4db9e0618b1396b1",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "2f5e2fcfbe552773",
        "name": "PISCINE OFF",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_off",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "switch.shelly1minig3_84fce6382750",
            "switch.electrolyseur",
            "switch.lumiere_piscine"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_off",
        "x": 1960,
        "y": 1640,
        "wires": [
            []
        ]
    },
    {
        "id": "36388a7c171e859c",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "2f5e2fcfbe552773",
        "name": "PLANIKA ON",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_on",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "switch.planika_on_switch_1"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_on",
        "x": 1960,
        "y": 1720,
        "wires": [
            []
        ]
    },
    {
        "id": "0c2a1314b121cfe2",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "2f5e2fcfbe552773",
        "name": "LEKTRICO OFF",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_on",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "button.lektrico_charge_stop"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_on",
        "x": 1960,
        "y": 1760,
        "wires": [
            []
        ]
    },
    {
        "id": "f7056c1092e92d0b",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "2f5e2fcfbe552773",
        "name": "LUMIERE EXTERIEURE",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_off",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "switch.interupteur_lumiere_exterieure"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_off",
        "x": 1990,
        "y": 1680,
        "wires": [
            []
        ]
    },
    {
        "id": "0600826375f8dd75",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "2f5e2fcfbe552773",
        "name": "SECHE SERVIETTE OFF",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_on",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "switch.seche_serviette"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_on",
        "x": 1990,
        "y": 1800,
        "wires": [
            []
        ]
    },
    {
        "id": "f504f8535b3332cd",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "2f5e2fcfbe552773",
        "name": "PRISES BUREAU OFF",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_off",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "switch.e2m_prise_bureau_switch",
            "switch.prise_fontaine",
            "switch.0xa4c1386129e10bd2"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_off",
        "x": 1990,
        "y": 1920,
        "wires": [
            []
        ]
    },
    {
        "id": "06bb7e8c739fefe7",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "2f5e2fcfbe552773",
        "name": "PRISES SALON OFF",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_off",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "switch.prise_frigo",
            "switch.prise_enfant"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_off",
        "x": 1980,
        "y": 2000,
        "wires": [
            []
        ]
    },
    {
        "id": "bf050c8d7f1f56a9",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "2f5e2fcfbe552773",
        "name": "PRISE CHAMBRE LEA OFF",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_off",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "switch.e2m_prise_chambre_lea_switch"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_off",
        "x": 2000,
        "y": 1960,
        "wires": [
            []
        ]
    },
    {
        "id": "d4d45c71f888bbb8",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "2f5e2fcfbe552773",
        "name": "PRISE CHAMBRE PARENTS OFF",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_off",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "switch.prise_chambre_parents"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_off",
        "x": 2020,
        "y": 1840,
        "wires": [
            []
        ]
    },
    {
        "id": "6d2550589dc092a8",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "2f5e2fcfbe552773",
        "name": "PRISE TELESCOPE OFF",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_off",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "switch.0xa4c1386129e10bd2"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_off",
        "x": 1990,
        "y": 1880,
        "wires": [
            []
        ]
    },
    {
        "id": "23193f1cf1ab4828",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "911ce4bfeaf3c135",
        "name": "CHAUFFE EAU ON",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_on",
        "floorId": [],
        "areaId": [],
        "deviceId": [
            "2d866641304c892f7f95cd4126e3bf7f"
        ],
        "entityId": [],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_on",
        "x": 1980,
        "y": 2100,
        "wires": [
            []
        ]
    },
    {
        "id": "3dc9dc2f1ee7e481",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "911ce4bfeaf3c135",
        "name": "PRISES FONTAINE BUREAU",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_off",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "switch.e2m_prise_bureau_switch",
            "switch.prise_fontaine",
            "switch.0xa4c1386129e10bd2"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_off",
        "x": 2010,
        "y": 2140,
        "wires": [
            []
        ]
    },
    {
        "id": "567cfc1553529b90",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "911ce4bfeaf3c135",
        "name": "PRISE_VMC_ON",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_on",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "switch.prise_vmc"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_on",
        "x": 1970,
        "y": 2240,
        "wires": [
            [
                "18172beef3583059"
            ]
        ]
    },
    {
        "id": "f83056ad321e8412",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "911ce4bfeaf3c135",
        "name": "PRISE CHAMBRE PARENTS ON",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "switch.turn_on",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "switch.prise_chambre_parents"
        ],
        "labelId": [],
        "data": "",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "switch",
        "service": "turn_on",
        "x": 2020,
        "y": 2180,
        "wires": [
            []
        ]
    },
    {
        "id": "f082998a84a2efd6",
        "type": "api-call-service",
        "z": "236bdcb1284426a5",
        "g": "911ce4bfeaf3c135",
        "name": "CODE_FONCTION_3",
        "server": "bb0a3ead.a33a3",
        "version": 7,
        "debugenabled": false,
        "action": "button.press",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "BOUTON_VMC_CODE_FONCTION_3"
        ],
        "labelId": [],
        "data": "[34102]",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": true,
        "domain": "button",
        "service": "press",
        "x": 2360,
        "y": 2240,
        "wires": [
            []
        ]
    },
    {
        "id": "18172beef3583059",
        "type": "delay",
        "z": "236bdcb1284426a5",
        "g": "911ce4bfeaf3c135",
        "name": "",
        "pauseType": "delay",
        "timeout": "5",
        "timeoutUnits": "minutes",
        "rate": "1",
        "nbRateUnits": "1",
        "rateUnits": "second",
        "randomFirst": "1",
        "randomLast": "5",
        "randomUnits": "seconds",
        "drop": false,
        "allowrate": false,
        "outputs": 1,
        "x": 2160,
        "y": 2240,
        "wires": [
            [
                "f082998a84a2efd6"
            ]
        ]
    },
    {
        "id": "7574af6c1288c89c",
        "type": "function",
        "z": "236bdcb1284426a5",
        "g": "fcc50eded02e8fc9",
        "name": "Dispatcher Input_Text",
        "func": "// ### CODE DU NŒUD : DISPATCHER INPUT TEXT - AMÉLIORÉ ###\n\ntry {\n    const command = msg.payload && typeof msg.payload === 'string' ? msg.payload.trim() : '';\n    \n    if (!command) {\n        node.warn(\"Commande invalide ou vide reçue par le dispatcher input_text\");\n        return null;\n    }\n\n    const helperNames = [\n        'mode_temperature',\n        'prechauffage',\n        'gestion_dynamique',\n        'marche_forcee',\n        'gestion_auto',\n        'utilisation_temp_exterieure',\n        'mi_saison'\n    ];\n\n    const regex = new RegExp(`^SET_INPUT_TEXT_(${helperNames.join('|')})_(.*)$`);\n    const match = command.match(regex);\n\n    if (match) {\n        const helperName = match[1];\n        let value = match[2];\n\n        if (helperName !== 'utilisation_temp_exterieure') {\n            value = value.replace(/_/g, ' ');\n        }\n        \n        const entityId = `input_text.nodered_etat_${helperName}`; \n\n        msg.payload = {\n            type: \"call_service\",\n            domain: \"input_text\",\n            service: \"set_value\",\n            service_data: {\n                value: value\n            },\n            target: {\n                entity_id: entityId\n            }\n        };\n        return msg;\n    }\n\n    return null;\n\n} catch (e) {\n    node.error(\"Erreur dans le dispatcher input_text : \" + e.message, msg);\n    return null;\n}",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1660,
        "y": 320,
        "wires": [
            [
                "14694fc768b31de9"
            ]
        ]
    },
    {
        "id": "8131d955371456a6",
        "type": "function",
        "z": "236bdcb1284426a5",
        "g": "02df0b2dbe863781",
        "name": "Logique Thermostats",
        "func": "// ============================================================\n// NŒUD : LOGIQUE THERMOSTATS - VERSION V8 NOVALIX\n// Type: Function\n// ============================================================\n// Ce nœud gère la température de toutes les pièces selon :\n// - Saison, présence, heure\n// - Gestion dynamique (solaire, consommation, température extérieure)\n// - Préchauffages programmables\n// - Gestion différenciée par personne\n//\n// ✅ NOUVEAUTÉS V8 :\n// - Sur-confort thermique pendant HC Novalix\n// - Suppression des restrictions jour Rouge pour Novalix\n// - Logique jour Rouge conservée uniquement pour Tempo\n// ============================================================\n\ntry {\n    const payload = msg.payload;\n    \n    const {\n        HEURE, MINUTE, METEO_JOUR, MAISON_INHABITEE, PROFIL_PRESENCE,\n        NIVEAU_BATTERIE_MAISON, CONSIGNES_ACTUELLES, ETATS_CLIM, DUREE_FORCAGE_MANUEL,\n        TEMP_EXTERIEURE_ACTUELLE, SURPLUS_PUISSANCE, TEMP_CONFORT_HIVER, TEMP_ECO_NUIT_HIVER,\n        TEMP_ECO_NUIT_ETE, TEMP_CONFORT_ETE, TEMP_HORS_GEL, TEMP_ECO_JOUR_ROUGE,\n        ACTIVATION_PRECHAUFFAGE_MATIN, HEURE_DEBUT_PRECHAUFFAGE_MATIN, HEURE_FIN_PRECHAUFFAGE_MATIN,\n        ACTIVATION_PRECHAUFFAGE_SOIR, HEURE_DEBUT_PRECHAUFFAGE_SOIR, HEURE_FIN_PRECHAUFFAGE_SOIR,\n        GESTION_AUTO_BUREAU, GESTION_AUTO_SALON, GESTION_AUTO_CH_PARENTS,\n        GESTION_AUTO_CH_LEA, GESTION_AUTO_CH_CELIA,\n        VENTILATION_AUTO_MI_SAISON, HEURE_DEBUT_VENTILATION, HEURE_FIN_VENTILATION,\n        AJUSTEMENT_TEMP_DYNAMIQUE, SEUIL_MODULATION_ECO, SEUIL_BOOST_SOLAIRE,\n        SEUIL_FROID_EXTERIEUR, SEUIL_CHAUD_EXTERIEUR, IS_NIGHT_BEFORE_RED_DAY,\n        LAST_TEMP_SET, OVERRIDE_TIMESTAMPS, PERSONNES_PRESENTES,\n        PUISSANCE_SOLAIRE_ACTUELLE, PUISSANCE_RESEAU,\n        GESTION_DYNAMIQUE_CONSO_ELEC, GESTION_DYNAMIQUE_TEMP_EXT,\n        MODE_SAISONNIER = 'INCONNU',\n        GESTION_PRESENCE_DIFFERENTIELLE = false,\n        GESTION_DIFF_ACTIVE_AUJOURDHUI = false,\n        HEURE_DEBUT_PAUSE_DEJEUNER = 12,\n        MINUTE_DEBUT_PAUSE_DEJEUNER = 0,\n        HEURE_FIN_PAUSE_DEJEUNER = 13,\n        MINUTE_FIN_PAUSE_DEJEUNER = 30,\n        HEURE_RETOUR_MODE_NORMAL = 17,\n        MINUTE_RETOUR_MODE_NORMAL = 30,\n        // ✅ NOUVEAUX PARAMÈTRES V8 NOVALIX\n        MODE_FOURNISSEUR = 'Tempo',\n        IS_HC_NOVALIX_NUIT = false,\n        IS_HC_NOVALIX_APREM = false,\n        NOVALIX_SURCONFORT_HC = 1.0,\n        COULEUR_TEMPO = null\n    } = payload;\n\n    msg.commands = msg.commands || [];\n\n    const PIECES = ['BUREAU', 'SALON', 'CHAMBRE_PARENTS', 'CHAMBRE_LEA', 'CHAMBRE_CELIA'];\n    const CHAMBRES_IDS = ['CHAMBRE_PARENTS', 'CHAMBRE_LEA', 'CHAMBRE_CELIA'];\n    const PIECES_VIE_IDS = ['BUREAU', 'SALON'];\n\n    payload.rapportAjustementTemp = \"Aucun\";\n    payload.rapportModeTemp = \"Aucun\";\n\n    // ============================================================\n    // ✅ V8 : MODE SÉCURITÉ (capteurs critiques manquants)\n    // ============================================================\n    if (payload.MODE_SECURITE) {\n        // En mode sécurité : températures éco partout, pas d'ajustements dynamiques\n        node.warn(`⚠️ Thermostats en mode sécurité ECO - Capteurs manquants : ${payload.CAPTEURS_MANQUANTS.join(', ')}`);\n        \n        const tempSecurite = (MODE_SAISONNIER === 'HIVER' || MODE_SAISONNIER === 'MI-SAISON-HIVER') \n            ? TEMP_ECO_NUIT_HIVER \n            : TEMP_ECO_NUIT_ETE;\n        \n        PIECES.forEach(piece => setClim(piece, 'ON', tempSecurite));\n        payload.rapportModeTemp = 'Securite_ECO';\n        payload.rapportChauffage = `Mode sécurité activé : températures en mode économique (${tempSecurite}°C) en attente de rétablissement des capteurs.`;\n        return msg;\n    }\n\n    // ============================================================\n    // FONCTION : CALCUL TEMPÉRATURE CONFORT DYNAMIQUE\n    // ✅ V8 : Ajout du sur-confort HC Novalix\n    // ============================================================\n    function getTempConfortDynamique(baseTemp, saison) {\n        let ajustementTotal = 0;\n        const ajustementReasons = [];\n        const isDaytime = (HEURE >= 7 && HEURE < 21);\n        const isConfortHours = (HEURE >= 5 && HEURE < 23);\n\n        // ✅ NOUVEAU V8 : Sur-confort pendant HC Novalix\n        if (MODE_FOURNISSEUR === 'Novalix' && NOVALIX_SURCONFORT_HC > 0) {\n            if (IS_HC_NOVALIX_NUIT || IS_HC_NOVALIX_APREM) {\n                ajustementReasons.push(`Surconfort_HC_Novalix`);\n                ajustementTotal += NOVALIX_SURCONFORT_HC;\n            }\n        }\n\n        if (AJUSTEMENT_TEMP_DYNAMIQUE > 0) {\n            if (GESTION_DYNAMIQUE_CONSO_ELEC) {\n                const conditionBoostSolaire = isDaytime && SURPLUS_PUISSANCE > SEUIL_BOOST_SOLAIRE;\n                if (conditionBoostSolaire) {\n                    ajustementReasons.push(\"Boost_Solaire\");\n                    if (saison === 'HIVER' || saison === 'MI-SAISON-HIVER') ajustementTotal += AJUSTEMENT_TEMP_DYNAMIQUE;\n                    else if (saison === 'ÉTÉ' || saison === 'MI-SAISON-ETE') ajustementTotal -= AJUSTEMENT_TEMP_DYNAMIQUE;\n                }\n\n                const conditionEcoModulation = PUISSANCE_RESEAU > 0 && PUISSANCE_RESEAU > SEUIL_MODULATION_ECO;\n                if (conditionEcoModulation) {\n                    ajustementReasons.push(\"Eco_Modulation\");\n                    if (saison === 'HIVER' || saison === 'MI-SAISON-HIVER') ajustementTotal -= AJUSTEMENT_TEMP_DYNAMIQUE;\n                    else if (saison === 'ÉTÉ' || saison === 'MI-SAISON-ETE') ajustementTotal += AJUSTEMENT_TEMP_DYNAMIQUE;\n                }\n            }\n\n            if (GESTION_DYNAMIQUE_TEMP_EXT && isConfortHours) {\n                const conditionFroidExterieur = (saison === 'HIVER' || saison === 'MI-SAISON-HIVER') && TEMP_EXTERIEURE_ACTUELLE < SEUIL_FROID_EXTERIEUR;\n                if (conditionFroidExterieur) {\n                    ajustementReasons.push(\"Froid_Extérieur\");\n                    ajustementTotal += AJUSTEMENT_TEMP_DYNAMIQUE;\n                }\n\n                const conditionChaleurExterieure = (saison === 'ÉTÉ' || saison === 'MI-SAISON-ETE') && TEMP_EXTERIEURE_ACTUELLE > SEUIL_CHAUD_EXTERIEUR;\n                if (conditionChaleurExterieure) {\n                    ajustementReasons.push(\"Chaleur_Extérieure\");\n                    ajustementTotal -= AJUSTEMENT_TEMP_DYNAMIQUE;\n                }\n            }\n        }\n\n        if (ajustementReasons.length > 0) {\n            payload.rapportAjustementTemp = ajustementReasons.join('+');\n        }\n\n        return baseTemp + ajustementTotal;\n    }\n\n    // ============================================================\n    // FONCTION : COMMANDE CLIMATISATION\n    // ============================================================\n    function setClim(piece, state, temp = null) {\n        const activationMap = {\n            'BUREAU': GESTION_AUTO_BUREAU,\n            'SALON': GESTION_AUTO_SALON,\n            'CHAMBRE_PARENTS': GESTION_AUTO_CH_PARENTS,\n            'CHAMBRE_LEA': GESTION_AUTO_CH_LEA,\n            'CHAMBRE_CELIA': GESTION_AUTO_CH_CELIA\n        };\n        if (!activationMap[piece]) return;\n\n        const tempIdeale = LAST_TEMP_SET[piece] || temp;\n        const etatActuel = ETATS_CLIM[piece];\n        const overrideTimestamp = OVERRIDE_TIMESTAMPS[piece];\n\n        let overrideEntitySuffix;\n        switch (piece) {\n            case 'BUREAU': overrideEntitySuffix = 'override_timestamp_bureau'; break;\n            case 'SALON': overrideEntitySuffix = 'override_timestamp_salon'; break;\n            case 'CHAMBRE_PARENTS': overrideEntitySuffix = 'override_timestamp_chambre_parents'; break;\n            case 'CHAMBRE_LEA': overrideEntitySuffix = 'override_timestamp_chambre_lea'; break;\n            case 'CHAMBRE_CELIA': overrideEntitySuffix = 'override_timestamp_chambre_celia'; break;\n            default: return;\n        }\n\n        const manualOverrideActive = (etatActuel !== 'off' && temp !== null && CONSIGNES_ACTUELLES[piece] !== tempIdeale);\n\n        if (manualOverrideActive) {\n            const now = Date.now();\n            let timestamp = overrideTimestamp ? parseInt(overrideTimestamp) : null;\n            if (!timestamp) {\n                timestamp = now;\n                msg.commands.push(`SET_INPUT_TEXT_${overrideEntitySuffix}_${timestamp}`);\n            }\n            const elapsedHours = (now - timestamp) / 3600000;\n            if (elapsedHours < DUREE_FORCAGE_MANUEL) return;\n            msg.commands.push(`SET_INPUT_TEXT_${overrideEntitySuffix}_`);\n        } else {\n            if (overrideTimestamp) msg.commands.push(`SET_INPUT_TEXT_${overrideEntitySuffix}_`);\n        }\n\n        if (state === 'ON') {\n            msg.commands.push(`THERMOSTAT_${piece}_ON`);\n            if (temp !== null) {\n                msg.commands.push(`${piece}_TEMP_${temp}`);\n                msg.commands.push(`SET_INPUT_NUMBER_last_temp_set_${piece.toLowerCase()}_${temp}`);\n            }\n        } else if (state === 'OFF') {\n            msg.commands.push(`THERMOSTAT_${piece}_OFF`);\n            if (temp !== null) {\n                msg.commands.push(`${piece}_TEMP_${temp}`);\n                msg.commands.push(`SET_INPUT_NUMBER_last_temp_set_${piece.toLowerCase()}_${temp}`);\n            }\n        }\n    }\n\n    // ============================================================\n    // SÉCURITÉ BATTERIE 16h (UNIQUEMENT TEMPO)\n    // ✅ V8 : Désactivé en mode Novalix\n    // ============================================================\n    if (MODE_FOURNISSEUR === 'Tempo' && COULEUR_TEMPO === 'Rouge' && HEURE >= 16 && HEURE < 17) {\n        const SEUIL_SECURITE_BATTERIE_16H = 50;\n        if (NIVEAU_BATTERIE_MAISON < SEUIL_SECURITE_BATTERIE_16H) {\n            PIECES.forEach(piece => setClim(piece, 'OFF', TEMP_HORS_GEL));\n            payload.rapportModeTemp = 'Securite_Batterie_16h';\n            payload.rapportChauffage = `Action de sécurité à 16h : batterie (${NIVEAU_BATTERIE_MAISON}%) sous le seuil de ${SEUIL_SECURITE_BATTERIE_16H}%. Chauffage coupé.`;\n            return msg;\n        }\n    }\n\n    // ============================================================\n    // FONCTION PRINCIPALE DE GESTION\n    // ============================================================\n    function gestionCommune() {\n        // Gestion piscine\n        if (MODE_SAISONNIER === 'ÉTÉ' || MODE_SAISONNIER === 'MI-SAISON-ETE') {\n            msg.commands.push('MODE_AUTO_PISCINE_ON');\n        } else {\n            msg.commands.push('MODE_AUTO_PISCINE_OFF');\n        }\n\n        // ✅ V8 : Logique jour Rouge uniquement en mode Tempo\n        let isDaytimeRedRestriction = false;\n        let isRedDaySunnyException = false;\n        \n        if (MODE_FOURNISSEUR === 'Tempo' && COULEUR_TEMPO === 'Rouge') {\n            isDaytimeRedRestriction = (HEURE >= 6 && HEURE < 22);\n            isRedDaySunnyException = isDaytimeRedRestriction && METEO_JOUR === 'BONNE' && !MAISON_INHABITEE;\n        }\n\n        // CAS 1 : MAISON INHABITÉE OU JOUR ROUGE RESTRICTIF (TEMPO UNIQUEMENT)\n        if (MAISON_INHABITEE || (isDaytimeRedRestriction && !isRedDaySunnyException)) {\n            PIECES.forEach(piece => setClim(piece, 'OFF', TEMP_HORS_GEL));\n            payload.rapportModeTemp = 'Hors_Gel';\n            msg.commands.push('VMC_MODE_VACANCES');\n        }\n        // CAS 2 : JOUR ROUGE ENSOLEILLÉ (TEMPO UNIQUEMENT)\n        else if (isRedDaySunnyException) {\n            if (PUISSANCE_SOLAIRE_ACTUELLE > 2000) {\n                let tempConfortRougeSolaire = getTempConfortDynamique(TEMP_ECO_JOUR_ROUGE, 'HIVER');\n                PIECES_VIE_IDS.forEach(piece => setClim(piece, 'ON', tempConfortRougeSolaire));\n                CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', TEMP_ECO_NUIT_HIVER));\n                payload.rapportModeTemp = 'Eco_Solaire_Rouge';\n                payload.rapportChauffage = `Jour Rouge ensoleillé : Chauffage des pièces de vie à ${tempConfortRougeSolaire.toFixed(1)}°C grâce au solaire (>2000W).`;\n            } else {\n                PIECES.forEach(piece => setClim(piece, 'OFF', TEMP_HORS_GEL));\n                payload.rapportModeTemp = 'Attente_Solaire_Rouge';\n                payload.rapportChauffage = `Jour Rouge ensoleillé : Chauffage en hors-gel en attente de production solaire suffisante (>2000W).`;\n            }\n            msg.commands.push('VMC_MODE_NORMAL');\n        }\n        // CAS 3 : FONCTIONNEMENT NORMAL\n        else {\n            msg.commands.push('VMC_MODE_NORMAL');\n\n            // MODE HIVER / MI-SAISON-HIVER\n            if (MODE_SAISONNIER === 'HIVER' || MODE_SAISONNIER === 'MI-SAISON-HIVER') {\n                let tempConfortHiverAjustee = getTempConfortDynamique(TEMP_CONFORT_HIVER, 'HIVER');\n                payload.TEMP_CONFORT_HIVER_AJUSTEE = tempConfortHiverAjustee;\n                PIECES.forEach(piece => msg.commands.push(`${piece}_MODE_CHAUFFAGE`));\n\n                let isPrechauffageMatin = false;\n                let isPrechauffageSoir = false;\n\n                // ✅ V8 : Préchauffage anticipé jour Rouge uniquement en mode Tempo\n                if (MODE_FOURNISSEUR === 'Tempo' && IS_NIGHT_BEFORE_RED_DAY) {\n                    if (HEURE >= 22 && HEURE < 23) {\n                        isPrechauffageSoir = true;\n                        payload.rapportContexte = `Préchauffage anticipé pour le jour rouge de demain.`;\n                    }\n                } else {\n                    if (ACTIVATION_PRECHAUFFAGE_MATIN && HEURE >= HEURE_DEBUT_PRECHAUFFAGE_MATIN && HEURE < HEURE_FIN_PRECHAUFFAGE_MATIN) {\n                        isPrechauffageMatin = true;\n                    }\n                    if (ACTIVATION_PRECHAUFFAGE_SOIR && HEURE >= HEURE_DEBUT_PRECHAUFFAGE_SOIR && HEURE < HEURE_FIN_PRECHAUFFAGE_SOIR) {\n                        isPrechauffageSoir = true;\n                    }\n                }\n\n                const isDeepNight = (HEURE >= 23 || HEURE < 5);\n\n                // NUIT PROFONDE (23h-5h)\n                if (isDeepNight) {\n                    CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', TEMP_ECO_NUIT_HIVER));\n                    PIECES_VIE_IDS.forEach(piece => setClim(piece, 'OFF', TEMP_HORS_GEL));\n                    payload.rapportModeTemp = 'Nuit_Eco';\n                }\n                // JOURNÉE / SOIRÉE (5h-23h)\n                else {\n                    if (GESTION_PRESENCE_DIFFERENTIELLE && GESTION_DIFF_ACTIVE_AUJOURDHUI) {\n                        const minutesActuelles = HEURE * 60 + MINUTE;\n                        const minutesDebutPause = HEURE_DEBUT_PAUSE_DEJEUNER * 60 + MINUTE_DEBUT_PAUSE_DEJEUNER;\n                        const minutesFinPause = HEURE_FIN_PAUSE_DEJEUNER * 60 + MINUTE_FIN_PAUSE_DEJEUNER;\n                        const minutesRetourNormal = HEURE_RETOUR_MODE_NORMAL * 60 + MINUTE_RETOUR_MODE_NORMAL;\n\n                        const isPauseDejeuner = (minutesActuelles >= minutesDebutPause && minutesActuelles < minutesFinPause);\n                        const isAvantRetourNormal = (minutesActuelles < minutesRetourNormal);\n\n                        switch (PROFIL_PRESENCE) {\n                            case 'YOHAN_SEUL':\n                                setClim('BUREAU', 'ON', tempConfortHiverAjustee);\n                                \n                                if (isPauseDejeuner) {\n                                    setClim('SALON', 'ON', tempConfortHiverAjustee);\n                                } else if (isAvantRetourNormal) {\n                                    setClim('SALON', 'ON', TEMP_ECO_NUIT_HIVER);\n                                } else {\n                                    setClim('SALON', 'ON', tempConfortHiverAjustee);\n                                }\n\n                                if (isPrechauffageMatin || isPrechauffageSoir) {\n                                    CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', tempConfortHiverAjustee));\n                                } else {\n                                    CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', TEMP_ECO_NUIT_HIVER));\n                                }\n\n                                if (isPauseDejeuner) {\n                                    payload.rapportModeTemp = 'Yohan_Seul_Pause_Dejeuner';\n                                } else if (isAvantRetourNormal) {\n                                    payload.rapportModeTemp = 'Yohan_Seul_Bureau';\n                                } else {\n                                    payload.rapportModeTemp = 'Yohan_Seul_Normal';\n                                }\n                                break;\n\n                            case 'JENNY_SEULE':\n                            case 'INVITE_SEUL':\n                                setClim('SALON', 'ON', tempConfortHiverAjustee);\n\n                                if (isPauseDejeuner) {\n                                    setClim('BUREAU', 'ON', tempConfortHiverAjustee);\n                                } else if (isAvantRetourNormal) {\n                                    setClim('BUREAU', 'ON', TEMP_ECO_NUIT_HIVER);\n                                } else {\n                                    setClim('BUREAU', 'ON', tempConfortHiverAjustee);\n                                }\n\n                                if (isPrechauffageMatin || isPrechauffageSoir) {\n                                    CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', tempConfortHiverAjustee));\n                                } else {\n                                    CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', TEMP_ECO_NUIT_HIVER));\n                                }\n\n                                if (isPauseDejeuner) {\n                                    payload.rapportModeTemp = PROFIL_PRESENCE === 'JENNY_SEULE' ? 'Jenny_Seule_Pause_Dejeuner' : 'Invite_Seul_Pause_Dejeuner';\n                                } else if (isAvantRetourNormal) {\n                                    payload.rapportModeTemp = PROFIL_PRESENCE === 'JENNY_SEULE' ? 'Jenny_Seule_Salon' : 'Invite_Seul_Salon';\n                                } else {\n                                    payload.rapportModeTemp = PROFIL_PRESENCE === 'JENNY_SEULE' ? 'Jenny_Seule_Normal' : 'Invite_Seul_Normal';\n                                }\n                                break;\n\n                            case 'COUPLE':\n                            case 'MIXTE':\n                            default:\n                                PIECES_VIE_IDS.forEach(piece => setClim(piece, 'ON', tempConfortHiverAjustee));\n                                payload.rapportModeTemp = 'Hiver_Confort';\n\n                                if (isPrechauffageMatin || isPrechauffageSoir) {\n                                    CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', tempConfortHiverAjustee));\n                                } else {\n                                    CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', TEMP_ECO_NUIT_HIVER));\n                                }\n                                break;\n                        }\n                    } else {\n                        PIECES_VIE_IDS.forEach(piece => setClim(piece, 'ON', tempConfortHiverAjustee));\n                        payload.rapportModeTemp = 'Hiver_Confort';\n\n                        if (isPrechauffageMatin || isPrechauffageSoir) {\n                            CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', tempConfortHiverAjustee));\n                        } else {\n                            CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', TEMP_ECO_NUIT_HIVER));\n                        }\n                    }\n                }\n            }\n            // MODE ÉTÉ / MI-SAISON-ÉTÉ\n            else if (MODE_SAISONNIER === 'ÉTÉ' || MODE_SAISONNIER === 'MI-SAISON-ETE') {\n                let tempConfortEteAjustee = getTempConfortDynamique(TEMP_CONFORT_ETE, 'ÉTÉ');\n                payload.TEMP_CONFORT_ETE_AJUSTEE = tempConfortEteAjustee;\n                PIECES.forEach(piece => msg.commands.push(`${piece}_MODE_CLIMATISATION`));\n\n                const isDeepNight = (HEURE >= 23 || HEURE < 5);\n\n                if (isDeepNight) {\n                    CHAMBRES_IDS.forEach(chambre => setClim(chambre, 'ON', TEMP_ECO_NUIT_ETE));\n                    PIECES_VIE_IDS.forEach(piece => setClim(piece, 'OFF', TEMP_HORS_GEL));\n                    payload.rapportModeTemp = 'Nuit_Eco_Ete';\n                } else {\n                    PIECES.forEach(piece => setClim(piece, 'ON', tempConfortEteAjustee));\n                    payload.rapportModeTemp = 'Ete_Confort';\n                }\n            }\n            // MODE INCONNU\n            else {\n                payload.rapportModeTemp = 'Off';\n                PIECES.forEach(piece => setClim(piece, 'OFF', TEMP_HORS_GEL));\n            }\n        }\n    }\n\n    gestionCommune();\n    return msg;\n\n} catch (e) {\n    node.error(e.stack, msg);\n    return null;\n}\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 540,
        "y": 400,
        "wires": [
            [
                "893895549972cd61",
                "48b93facf3bde2b0"
            ]
        ]
    },
    {
        "id": "893895549972cd61",
        "type": "function",
        "z": "236bdcb1284426a5",
        "g": "02df0b2dbe863781",
        "name": "Logique Véhicule",
        "func": "// ============================================================\n// NŒUD : LOGIQUE VÉHICULE - VERSION V8 NOVALIX\n// Type: Function\n// ============================================================\n// Ce nœud gère la charge de la voiture électrique selon :\n// - Le fournisseur d'électricité (Tempo / Novalix)\n// - Les plages horaires creuses\n// - Le surplus solaire\n// - Le mode forcé manuel\n//\n// ✅ NOUVEAUTÉS V8 :\n// - Adaptation aux plages HC Novalix (01h-07h + 14h30-16h30)\n// - Logique Tempo conservée pour compatibilité\n// ============================================================\n\ntry {\n    const payload = msg.payload;\n    \n    const {\n        HEURE, NIVEAU_BATTERIE_VOITURE,\n        CONSIGNE_CHARGE_VOITURE, SURPLUS_PUISSANCE, SEUIL_SURPLUS_SOLAIRE_VOITURE,\n        MAISON_INHABITEE, VOITURE_CHARGE_FORCEE,\n        // ✅ NOUVEAUX PARAMÈTRES V8\n        MODE_FOURNISSEUR = 'Tempo',\n        IS_HC_NOVALIX_NUIT = false,\n        IS_HC_NOVALIX_APREM = false,\n        COULEUR_TEMPO = null\n    } = payload;\n\n    msg.commands = msg.commands || [];\n\n    // --- LOGIQUE DE FORÇAGE PRIORITAIRE ---\n    if (VOITURE_CHARGE_FORCEE) {\n        if (NIVEAU_BATTERIE_VOITURE >= 100) {\n            msg.commands.push('CHARGE_VOITURE_OFF');\n            msg.commands.push('TURN_OFF_input_boolean.voiture_charge_forcee');\n        } else {\n            msg.commands.push('CHARGE_VOITURE_ON');\n        }\n        return msg;\n    }\n\n    if (MAISON_INHABITEE) {\n        msg.commands.push('CHARGE_VOITURE_OFF');\n        return msg;\n    }\n\n    if (NIVEAU_BATTERIE_VOITURE === -1) {\n        msg.commands.push('CHARGE_VOITURE_OFF');\n        return msg;\n    }\n\n    const currentCarBattery = NIVEAU_BATTERIE_VOITURE;\n\n    // Si batterie voiture >= consigne, on arrête la charge\n    if (currentCarBattery >= CONSIGNE_CHARGE_VOITURE) {\n        msg.commands.push('CHARGE_VOITURE_OFF');\n        return msg;\n    }\n\n    // ========================================================\n    // MODE TEMPO (ANCIEN COMPORTEMENT)\n    // ========================================================\n    if (MODE_FOURNISSEUR === 'Tempo') {\n        const isOffPeakHours = HEURE >= 22 || HEURE < 6;\n\n        switch (COULEUR_TEMPO) {\n            case 'Rouge':\n            case 'Bleu':\n                if (isOffPeakHours) { \n                    msg.commands.push('CHARGE_VOITURE_ON'); \n                } else { \n                    msg.commands.push('CHARGE_VOITURE_OFF'); \n                }\n                break;\n            case 'Blanc':\n            default:\n                if (isOffPeakHours || SURPLUS_PUISSANCE > SEUIL_SURPLUS_SOLAIRE_VOITURE) {\n                    msg.commands.push('CHARGE_VOITURE_ON');\n                } else {\n                    msg.commands.push('CHARGE_VOITURE_OFF');\n                }\n                break;\n        }\n        return msg;\n    }\n\n    // ========================================================\n    // MODE NOVALIX (NOUVELLE STRATÉGIE)\n    // ========================================================\n    else if (MODE_FOURNISSEUR === 'Novalix') {\n        // Charge pendant les HC ou en cas de surplus solaire suffisant\n        if (IS_HC_NOVALIX_NUIT || IS_HC_NOVALIX_APREM || SURPLUS_PUISSANCE > SEUIL_SURPLUS_SOLAIRE_VOITURE) {\n            msg.commands.push('CHARGE_VOITURE_ON');\n        } else {\n            msg.commands.push('CHARGE_VOITURE_OFF');\n        }\n        return msg;\n    }\n\n    // Par défaut (ne devrait jamais arriver)\n    msg.commands.push('CHARGE_VOITURE_OFF');\n    return msg;\n\n} catch (e) {\n    node.error(e.stack, msg);\n    return null;\n}\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 530,
        "y": 460,
        "wires": [
            [
                "87514397dca3f3be",
                "94efe42faad5ce9c"
            ]
        ]
    },
    {
        "id": "87514397dca3f3be",
        "type": "function",
        "z": "236bdcb1284426a5",
        "g": "02df0b2dbe863781",
        "name": "Logique Batterie",
        "func": "// ============================================================\n// NŒUD : LOGIQUE BATTERIE - VERSION V8 NOVALIX\n// Type: Function\n// ============================================================\n// Ce nœud gère la stratégie de charge/décharge de la batterie\n// domestique selon le fournisseur d'électricité sélectionné.\n//\n// ✅ NOUVEAUTÉS V8 :\n// - Support double fournisseur (Tempo / Novalix)\n// - Stratégie Novalix avec 2 plages HC (nuit + après-midi)\n// - Recharge complémentaire après-midi si batterie < seuil\n// - Suppression logique ECONOMIE_ECONOMIE_ON/OFF en mode Novalix\n// ============================================================\n\ntry {  \n    const payload = msg.payload;  \n\n    const {\n        HEURE, MINUTE, METEO_DEMAIN, MODE_SAISONNIER,\n        IS_NIGHT_BEFORE_RED_DAY, GESTION_AUTO_BATTERIE, MODE_FORCE_BATTERIE_SELECT,\n        NIVEAU_BATTERIE_MAISON, SEUIL_BATTERIE_AUTO_SOLAIRE_NUIT,\n        MODE_FOURNISSEUR, IS_HC_NOVALIX_NUIT, IS_HC_NOVALIX_APREM,\n        NOVALIX_SEUIL_RECHARGE_APREM, COULEUR_TEMPO\n    } = payload;\n\n    msg.commands = msg.commands || [];\n    payload.rapportActionBatterie = \"AUTO\"; // Valeur par défaut\n\n    // ============================================================\n    // ✅ V8 : MODE SÉCURITÉ (capteurs critiques manquants)\n    // ============================================================\n    if (payload.MODE_SECURITE) {\n        // En mode sécurité : batterie en AUTO uniquement, pas de charge/décharge forcée\n        msg.commands.push('PACK_BATTERIE_AUTO');\n        payload.rapportActionBatterie = \"SECURITE_AUTO\";\n        node.warn(`⚠️ Batterie en mode sécurité AUTO - Capteurs manquants : ${payload.CAPTEURS_MANQUANTS.join(', ')}`);\n        return msg;\n    }\n\n    // --- LOGIQUE AUTOMATIQUE ---\n    if (GESTION_AUTO_BATTERIE) {\n        \n        // ========================================================\n        // MODE TEMPO (ANCIEN COMPORTEMENT)\n        // ========================================================\n        if (MODE_FOURNISSEUR === 'Tempo') {\n            const isNightTime = HEURE >= 22 || HEURE < 6;\n\n            // --- GESTION NOCTURNE (22h-06h) ---\n            if (isNightTime) {\n                // PRIORITÉ 1 : Anticiper un jour Rouge\n                if (IS_NIGHT_BEFORE_RED_DAY) {\n                    msg.commands.push('PACK_BATTERIE_CHARGE');\n                    payload.rapportActionBatterie = \"CHARGE_ANTICIPEE_ROUGE\";\n                    return msg;\n                }\n\n                // PRIORITÉ 2 : Stratégie \"Solaire\" (Été / Mi-saison été)\n                if (MODE_SAISONNIER === 'ÉTÉ' || MODE_SAISONNIER === 'MI-SAISON-ETE') {\n                    if (METEO_DEMAIN === 'BONNE') {\n                        if (NIVEAU_BATTERIE_MAISON >= SEUIL_BATTERIE_AUTO_SOLAIRE_NUIT) {\n                            msg.commands.push('PACK_BATTERIE_AUTO');\n                            payload.rapportActionBatterie = \"AUTO_POUR_SOLAIRE\";\n                        } else {\n                            msg.commands.push('PACK_BATTERIE_CHARGE');\n                            payload.rapportActionBatterie = \"CHARGE_MINIMALE_SOLAIRE\";\n                        }\n                        return msg;\n                    } else if (METEO_DEMAIN === 'MAUVAISE') {\n                        msg.commands.push('PACK_BATTERIE_CHARGE');\n                        payload.rapportActionBatterie = \"CHARGE_MAUVAIS_TEMPS\";\n                        return msg;\n                    } else {\n                        msg.commands.push('PACK_BATTERIE_CHARGE');\n                        payload.rapportActionBatterie = \"CHARGE_METEO_INCONNUE\";\n                        return msg;\n                    }\n                }\n                // PRIORITÉ 3 : Stratégie \"Hiver\"\n                msg.commands.push('PACK_BATTERIE_CHARGE');\n                payload.rapportActionBatterie = \"CHARGE_HIVER\";\n                return msg;\n            }\n\n            // --- GESTION JOURNÉE (06h-22h) ---\n            else {\n                if (COULEUR_TEMPO === 'Rouge') {\n                    payload.rapportActionBatterie = \"DECHARGE_JOUR_ROUGE\";\n                }\n                msg.commands.push('PACK_BATTERIE_AUTO');\n            }\n\n            // Logique ECONOMIE_ECONOMIE_ON/OFF pour les jours rouges Tempo\n            if (COULEUR_TEMPO === 'Rouge') {\n                if (HEURE === 6 && MINUTE < 5) { msg.commands.push('ECONOMIE_ECONOMIE_ON'); }\n                if (HEURE === 22 && MINUTE < 5) { msg.commands.push('ECONOMIE_ECONOMIE_OFF'); }\n            }\n            return msg;\n        }\n        \n        // ========================================================\n        // MODE NOVALIX (NOUVELLE STRATÉGIE SIMPLIFIÉE)\n        // ========================================================\n        else if (MODE_FOURNISSEUR === 'Novalix') {\n            \n            // --- GESTION HC NUIT (01h-07h) ---\n            // Stratégie simple : TOUJOURS charger pour avoir 100% le matin\n            // Pas besoin d'intelligence météo car on a la plage après-midi en complément\n            if (IS_HC_NOVALIX_NUIT) {\n                msg.commands.push('PACK_BATTERIE_CHARGE');\n                payload.rapportActionBatterie = \"NOVALIX_CHARGE_NUIT\";\n                return msg;\n            }\n            \n            // --- GESTION HC APRÈS-MIDI (14h30-16h30) ✨ NOUVEAU ---\n            else if (IS_HC_NOVALIX_APREM) {\n                // Recharge complémentaire si batterie sous le seuil\n                if (NIVEAU_BATTERIE_MAISON < NOVALIX_SEUIL_RECHARGE_APREM) {\n                    msg.commands.push('PACK_BATTERIE_CHARGE');\n                    payload.rapportActionBatterie = \"NOVALIX_RECHARGE_APREM\";\n                } else {\n                    // Batterie suffisante, on laisse en auto pour utiliser le solaire\n                    msg.commands.push('PACK_BATTERIE_AUTO');\n                    payload.rapportActionBatterie = \"NOVALIX_AUTO_APREM_OK\";\n                }\n                return msg;\n            }\n            \n            // --- GESTION HEURES PLEINES ---\n            else {\n                // En dehors des HC, toujours en mode AUTO\n                msg.commands.push('PACK_BATTERIE_AUTO');\n                payload.rapportActionBatterie = \"NOVALIX_AUTO_HP\";\n                return msg;\n            }\n        }\n    }\n    \n    // --- LOGIQUE MANUELLE (inchangée) ---\n    else {\n        let forcedModeCommand;\n        switch (MODE_FORCE_BATTERIE_SELECT) {\n            case 'Charge':\n                forcedModeCommand = 'PACK_BATTERIE_CHARGE';\n                payload.rapportActionBatterie = \"FORCE_CHARGE\";\n                break;\n            case 'Equalization':\n                forcedModeCommand = 'PACK_BATTERIE_EQUALIZATION';\n                payload.rapportActionBatterie = \"FORCE_EQUALIZATION\";\n                break;\n            case 'Discharge':\n                forcedModeCommand = 'PACK_BATTERIE_DISCHARGE';\n                payload.rapportActionBatterie = \"FORCE_DECHARGE\";\n                break;\n            case 'Standby':\n                forcedModeCommand = 'PACK_BATTERIE_STANDBY';\n                payload.rapportActionBatterie = \"FORCE_STANDBY\";\n                break;\n            case 'Auto':\n                forcedModeCommand = 'PACK_BATTERIE_AUTO';\n                payload.rapportActionBatterie = \"AUTO_ONDULEUR_INTERNE\";\n                break;\n            default:\n                forcedModeCommand = 'PACK_BATTERIE_AUTO';\n                payload.rapportActionBatterie = \"AUTO_ONDULEUR_INTERNE\";\n                break;\n        }\n        msg.commands.push(forcedModeCommand);\n        return msg;\n    }\n\n} catch (e) {  \n    node.error(e.stack, msg);  \n    return null;  \n}\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 520,
        "y": 520,
        "wires": [
            [
                "0310f42df5aa96be",
                "9b50dd24a4eed72a"
            ]
        ]
    },
    {
        "id": "0310f42df5aa96be",
        "type": "function",
        "z": "236bdcb1284426a5",
        "g": "02df0b2dbe863781",
        "name": "Préparation rapport et Finalisation",
        "func": "// ============================================================\n// NŒUD : PRÉPARATION RAPPORT ET FINALISATION - VERSION V8\n// Type: Function\n// ============================================================\n// Ce nœud génère le rapport quotidien avec toutes les infos\n// et met à jour les helpers d'état dans Home Assistant.\n//\n// ✅ NOUVEAUTÉS V8 :\n// - Mention du fournisseur d'électricité actif\n// - Rapports adaptés aux stratégies Tempo/Novalix\n// - Suppression des mentions jour Rouge pour Novalix\n// ============================================================\n\ntry {\n    const payload = msg.payload;\n    const {\n        HEURE, METEO_JOUR,\n        NIVEAU_BATTERIE_VOITURE, NIVEAU_BATTERIE_MAISON, TEMP_CONFORT_HIVER, TEMP_ECO_NUIT_HIVER,\n        TEMP_CONFORT_ETE, TEMP_ECO_NUIT_ETE, ACTIVATION_PRECHAUFFAGE_MATIN, HEURE_DEBUT_PRECHAUFFAGE_MATIN,\n        HEURE_FIN_PRECHAUFFAGE_MATIN, ACTIVATION_PRECHAUFFAGE_SOIR, HEURE_DEBUT_PRECHAUFFAGE_SOIR,\n        HEURE_FIN_PRECHAUFFAGE_SOIR, GESTION_AUTO_BUREAU,\n        GESTION_AUTO_SALON, GESTION_AUTO_CH_PARENTS, GESTION_AUTO_CH_LEA, GESTION_AUTO_CH_CELIA,\n        VENTILATION_AUTO_MI_SAISON, HEURE_DEBUT_VENTILATION, HEURE_FIN_VENTILATION,\n        AJUSTEMENT_TEMP_DYNAMIQUE, DUREE_FORCAGE_MANUEL, SEUIL_FROID_EXTERIEUR, SEUIL_CHAUD_EXTERIEUR,\n        rapportModeTemp, rapportAjustementTemp, rapportActionBatterie, TEMP_EXTERIEURE_ACTUELLE,\n        TEMP_MAX_PREVUE, IS_NIGHT_BEFORE_RED_DAY, GESTION_AUTO_BATTERIE,\n        CONSIGNE_CHARGE_VOITURE, TEMP_ECO_JOUR_ROUGE, VOITURE_CHARGE_FORCEE,\n        SEUIL_BATTERIE_AUTO_SOLAIRE_NUIT, MODE_SAISONNIER, METEO_DEMAIN,\n        TEMP_CONFORT_HIVER_AJUSTEE = null, TEMP_CONFORT_ETE_AJUSTEE = null,\n        SENSOR_ERRORS = [], PROFIL_PRESENCE = 'ABSENT', PERSONNES_PRESENTES = [],\n        MAISON_INHABITEE = false, HEURE_RETOUR_MODE_NORMAL = 17,\n        MINUTE_RETOUR_MODE_NORMAL = 30, HEURE_DEBUT_PAUSE_DEJEUNER = 12,\n        MINUTE_DEBUT_PAUSE_DEJEUNER = 0, HEURE_FIN_PAUSE_DEJEUNER = 13,\n        MINUTE_FIN_PAUSE_DEJEUNER = 30, MINUTE = 0,\n        // ✅ NOUVEAUX PARAMÈTRES V8\n        MODE_FOURNISSEUR = 'Tempo',\n        COULEUR_TEMPO = null\n    } = payload;\n\n    msg.commands = msg.commands || [];\n\n    const safeRapportModeTemp = rapportModeTemp || \"Mode Inconnu\";\n    const safeRapportAjustementTemp = rapportAjustementTemp || \"Aucun\";\n\n    // --- MISE À JOUR DES HELPERS ---\n    const isPrechauffageMatinActif = (ACTIVATION_PRECHAUFFAGE_MATIN && HEURE >= HEURE_DEBUT_PRECHAUFFAGE_MATIN && HEURE < HEURE_FIN_PRECHAUFFAGE_MATIN);\n    const isPrechauffageSoirActif = (ACTIVATION_PRECHAUFFAGE_SOIR && HEURE >= HEURE_DEBUT_PRECHAUFFAGE_SOIR && HEURE < HEURE_FIN_PRECHAUFFAGE_SOIR);\n    const isPrechauffageActif = isPrechauffageMatinActif || isPrechauffageSoirActif;\n    msg.commands.push(`SET_INPUT_TEXT_mode_temperature_${safeRapportModeTemp}`);\n    msg.commands.push(`SET_INPUT_TEXT_prechauffage_${isPrechauffageActif ? 'On' : 'Off'}`);\n    msg.commands.push(`SET_INPUT_TEXT_gestion_dynamique_${(safeRapportAjustementTemp !== \"Aucun\") ? `On_${AJUSTEMENT_TEMP_DYNAMIQUE}°` : 'Off'}`);\n    msg.commands.push(`SET_INPUT_TEXT_marche_forcee_${(DUREE_FORCAGE_MANUEL > 0) ? `On_${DUREE_FORCAGE_MANUEL}h` : 'Off'}`);\n    const allAuto = GESTION_AUTO_BUREAU && GESTION_AUTO_SALON && GESTION_AUTO_CH_PARENTS && GESTION_AUTO_CH_LEA && GESTION_AUTO_CH_CELIA;\n    msg.commands.push(`SET_INPUT_TEXT_gestion_auto_${allAuto ? 'On' : 'Off'}`);\n    const isTempExtAjustement = safeRapportAjustementTemp.includes(\"Froid\") || safeRapportAjustementTemp.includes(\"Chaleur\");\n    msg.commands.push(`SET_INPUT_TEXT_utilisation_temp_exterieure_${isTempExtAjustement ? `On_[${SEUIL_FROID_EXTERIEUR},${SEUIL_CHAUD_EXTERIEUR}]` : 'Off'}`);\n    msg.commands.push(`SET_INPUT_TEXT_mi_saison_${VENTILATION_AUTO_MI_SAISON ? `On_[${HEURE_DEBUT_VENTILATION}h-${HEURE_FIN_VENTILATION}h]` : 'Off'}`);\n\n    // ============================================================\n    // GÉNÉRATION DU RAPPORT DE PRÉSENCE\n    // ============================================================\n    let rapportPresence = \"\";\n    if (MAISON_INHABITEE) {\n        rapportPresence = \"🏚️ Maison inhabitée\";\n    } else {\n        const emojiMap = {\n            'YOHAN': '👤',\n            'JENNY': '👩',\n            'INVITE': '👥'\n        };\n        const listePersonnes = PERSONNES_PRESENTES.map(p => `${emojiMap[p] || '👤'} ${p.charAt(0) + p.slice(1).toLowerCase()}`).join(', ');\n        rapportPresence = `🏠 Présence détectée : ${listePersonnes}`;\n        \n        let messageSpecifique = \"\";\n        const heureRetourFormatee = (MINUTE_RETOUR_MODE_NORMAL === 0)\n            ? `${HEURE_RETOUR_MODE_NORMAL}h`\n            : `${HEURE_RETOUR_MODE_NORMAL}h${MINUTE_RETOUR_MODE_NORMAL.toString().padStart(2, '0')}`;\n        \n        const minutesActuelles = HEURE * 60 + MINUTE;\n        const minutesRetourNormal = HEURE_RETOUR_MODE_NORMAL * 60 + MINUTE_RETOUR_MODE_NORMAL;\n        const minutesDebutPause = HEURE_DEBUT_PAUSE_DEJEUNER * 60 + MINUTE_DEBUT_PAUSE_DEJEUNER;\n        const minutesFinPause = HEURE_FIN_PAUSE_DEJEUNER * 60 + MINUTE_FIN_PAUSE_DEJEUNER;\n        \n        const isAvantRetourNormal = minutesActuelles < minutesRetourNormal;\n        const isPauseDejeuner = (minutesActuelles >= minutesDebutPause && minutesActuelles < minutesFinPause);\n\n        switch(PROFIL_PRESENCE) {\n            case 'YOHAN_SEUL':\n                if (isPauseDejeuner) {\n                    messageSpecifique = \"En pause déjeuner : Bureau et Salon en confort\";\n                } else if (isAvantRetourNormal) {\n                    messageSpecifique = `Configuration adaptée : Bureau confort, Salon économie jusqu'à ${heureRetourFormatee}`;\n                }\n                break;\n\n            case 'JENNY_SEULE':\n                if (isPauseDejeuner) {\n                    messageSpecifique = \"En pause déjeuner : Salon et Bureau en confort\";\n                } else if (isAvantRetourNormal) {\n                    messageSpecifique = `Configuration adaptée : Salon confort, Bureau économie jusqu'à ${heureRetourFormatee}`;\n                }\n                break;\n\n            case 'INVITE_SEUL':\n                if (isPauseDejeuner) {\n                    messageSpecifique = \"En pause déjeuner : Salon et Bureau en confort\";\n                } else if (isAvantRetourNormal) {\n                    messageSpecifique = `Configuration adaptée : Salon confort, Bureau économie jusqu'à ${heureRetourFormatee}`;\n                }\n                break;\n\n            case 'COUPLE':\n                messageSpecifique = \"Configuration normale : Tous les espaces actifs\";\n                break;\n\n            case 'MIXTE':\n                messageSpecifique = \"Configuration normale : Présence multiple détectée\";\n                break;\n\n            default:\n                messageSpecifique = \"\";\n        }\n\n        if (messageSpecifique) {\n            rapportPresence += `\\nℹ️ ${messageSpecifique}`;\n        }\n    }\n    payload.rapportPresence = rapportPresence;\n\n    // ============================================================\n    // ✅ V8 : GÉNÉRATION RAPPORT ALERTES MODE SÉCURITÉ\n    // ============================================================\n    if (payload.MODE_SECURITE && payload.CAPTEURS_MANQUANTS && payload.CAPTEURS_MANQUANTS.length > 0) {\n        const listeCapteurs = payload.CAPTEURS_MANQUANTS.map(c => `- ${c.replace(/_/g, ' ')}`).join('\\n');\n        payload.rapportAlertes = `🚨 **Mode Sécurité Activé**\\n\\nCapteurs indisponibles détectés :\\n${listeCapteurs}\\n\\n` +\n            `Actions conservatrices appliquées :\\n` +\n            `- Batterie en mode automatique (pas de charge/décharge forcée)\\n` +\n            `- Thermostats en mode économique\\n` +\n            `- Charge véhicule désactivée\\n\\n` +\n            `Le système reprendra son fonctionnement normal dès le rétablissement des capteurs.`;\n    } else {\n        payload.rapportAlertes = \"\";\n    }\n\n    // ============================================================\n    // ENRICHISSEMENT DU PAYLOAD POUR LE RAPPORT\n    // ============================================================\n    const salutation = (HEURE < 18) ? \"Bonjour\" : \"Bonsoir\";\n    payload.rapportSalutation = `${salutation} !`;\n    \n    // ✅ V8 : Mention du fournisseur d'électricité\n    payload.rapportFournisseur = `📊 Fournisseur : **${MODE_FOURNISSEUR}**`;\n    \n    if (TEMP_MAX_PREVUE !== null) {\n        payload.rapportMeteoActuelle = `Actuellement, il fait ${TEMP_EXTERIEURE_ACTUELLE}°C dehors. La température maximale attendue aujourd'hui est de ${TEMP_MAX_PREVUE}°C.`;\n    } else {\n        payload.rapportMeteoActuelle = `Actuellement, il fait ${TEMP_EXTERIEURE_ACTUELLE}°C dehors. La prévision de température maximale n'est pas disponible.`;\n    }\n    \n    // ✅ V8 : Titre adapté selon le fournisseur\n    let pastille = '';\n    if (MODE_FOURNISSEUR === 'Tempo' && COULEUR_TEMPO) {\n        if (COULEUR_TEMPO === 'Bleu') pastille = '🔵 ';\n        else if (COULEUR_TEMPO === 'Blanc') pastille = '⚪ ';\n        else if (COULEUR_TEMPO === 'Rouge') pastille = '🔴 ';\n        payload.rapportTitre = `${pastille}Jour ${COULEUR_TEMPO} en mode ${safeRapportModeTemp.replace(/_/g, ' ')}`;\n    } else {\n        payload.rapportTitre = `Mode ${safeRapportModeTemp.replace(/_/g, ' ')}`;\n    }\n    \n    payload.rapportContexte = \"\";\n    if (isPrechauffageMatinActif) {\n        payload.rapportContexte = `Le préchauffage du matin est en cours jusqu'à ${HEURE_FIN_PRECHAUFFAGE_MATIN}h.`;\n    } else if (isPrechauffageSoirActif) {\n        payload.rapportContexte = `Le préchauffage du soir est en cours jusqu'à ${HEURE_FIN_PRECHAUFFAGE_SOIR}h.`;\n    }\n  \n    // --- LOGIQUE RAPPORT CHAUFFAGE ---\n    if (['Securite Batterie 16h', 'Attente Solaire Rouge'].includes(safeRapportModeTemp)) {\n        if (!payload.rapportChauffage) {\n            payload.rapportChauffage = \"Mode sécurité activé.\";\n        }\n    }\n    else if (safeRapportModeTemp === 'Yohan_Seul_Pause_Dejeuner') {\n        const tempConfortAffichee = (safeRapportAjustementTemp !== \"Aucun\" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;\n        payload.rapportChauffage = `Pause déjeuner : Bureau et Salon en confort à ${tempConfortAffichee.toFixed(1)}°C, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;\n    } else if (safeRapportModeTemp === 'Yohan_Seul_Bureau') {\n        const tempConfortAffichee = (safeRapportAjustementTemp !== \"Aucun\" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;\n        payload.rapportChauffage = `Bureau en confort à ${tempConfortAffichee.toFixed(1)}°C, Salon en économie à ${TEMP_ECO_NUIT_HIVER}°C, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;\n    } else if (safeRapportModeTemp === 'Yohan_Seul_Normal') {\n        const tempConfortAffichee = (safeRapportAjustementTemp !== \"Aucun\" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;\n        const minutesActuelles = HEURE * 60 + MINUTE;\n        const minutesRetourNormal = HEURE_RETOUR_MODE_NORMAL * 60 + MINUTE_RETOUR_MODE_NORMAL;\n        const mentionRetour = (minutesActuelles >= minutesRetourNormal && minutesActuelles < minutesRetourNormal + 10) ? ' (retour mode normal)' : '';\n        payload.rapportChauffage = `Bureau et Salon en confort à ${tempConfortAffichee.toFixed(1)}°C${mentionRetour}, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;\n    }\n    else if (safeRapportModeTemp === 'Jenny_Seule_Pause_Dejeuner') {\n        const tempConfortAffichee = (safeRapportAjustementTemp !== \"Aucun\" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;\n        payload.rapportChauffage = `Pause déjeuner : Salon et Bureau en confort à ${tempConfortAffichee.toFixed(1)}°C, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;\n    } else if (safeRapportModeTemp === 'Jenny_Seule_Salon') {\n        const tempConfortAffichee = (safeRapportAjustementTemp !== \"Aucun\" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;\n        payload.rapportChauffage = `Salon en confort à ${tempConfortAffichee.toFixed(1)}°C, Bureau en économie à ${TEMP_ECO_NUIT_HIVER}°C, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;\n    } else if (safeRapportModeTemp === 'Jenny_Seule_Normal') {\n        const tempConfortAffichee = (safeRapportAjustementTemp !== \"Aucun\" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;\n        const minutesActuelles = HEURE * 60 + MINUTE;\n        const minutesRetourNormal = HEURE_RETOUR_MODE_NORMAL * 60 + MINUTE_RETOUR_MODE_NORMAL;\n        const mentionRetour = (minutesActuelles >= minutesRetourNormal && minutesActuelles < minutesRetourNormal + 10) ? ' (retour mode normal)' : '';\n        payload.rapportChauffage = `Salon et Bureau en confort à ${tempConfortAffichee.toFixed(1)}°C${mentionRetour}, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;\n    }\n    else if (safeRapportModeTemp === 'Invite_Seul_Pause_Dejeuner') {\n        const tempConfortAffichee = (safeRapportAjustementTemp !== \"Aucun\" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;\n        payload.rapportChauffage = `Pause déjeuner : Salon et Bureau en confort à ${tempConfortAffichee.toFixed(1)}°C, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;\n    } else if (safeRapportModeTemp === 'Invite_Seul_Salon') {\n        const tempConfortAffichee = (safeRapportAjustementTemp !== \"Aucun\" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;\n        payload.rapportChauffage = `Salon en confort à ${tempConfortAffichee.toFixed(1)}°C, Bureau en économie à ${TEMP_ECO_NUIT_HIVER}°C, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;\n    } else if (safeRapportModeTemp === 'Invite_Seul_Normal') {\n        const tempConfortAffichee = (safeRapportAjustementTemp !== \"Aucun\" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;\n        const minutesActuelles = HEURE * 60 + MINUTE;\n        const minutesRetourNormal = HEURE_RETOUR_MODE_NORMAL * 60 + MINUTE_RETOUR_MODE_NORMAL;\n        const mentionRetour = (minutesActuelles >= minutesRetourNormal && minutesActuelles < minutesRetourNormal + 10) ? ' (retour mode normal)' : '';\n        payload.rapportChauffage = `Salon et Bureau en confort à ${tempConfortAffichee.toFixed(1)}°C${mentionRetour}, chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;\n    }\n    else if (safeRapportModeTemp === 'Hiver_Confort' || safeRapportModeTemp === 'Hiver Confort') {\n        const tempConfortAffichee = (safeRapportAjustementTemp !== \"Aucun\" && TEMP_CONFORT_HIVER_AJUSTEE !== null) ? TEMP_CONFORT_HIVER_AJUSTEE : TEMP_CONFORT_HIVER;\n        const ajustementText = (safeRapportAjustementTemp !== \"Aucun\") ? ` (ajustée via ${safeRapportAjustementTemp.replace(/_/g, ' ')})` : '';\n        payload.rapportChauffage = `Les pièces de vie sont réglées autour de ${tempConfortAffichee.toFixed(1)}°C${ajustementText} et les chambres à ${TEMP_ECO_NUIT_HIVER}°C.`;\n    } else if (safeRapportModeTemp === 'Nuit_Eco' || safeRapportModeTemp === 'Nuit Eco') {\n        payload.rapportChauffage = `Les chambres sont chauffées à ${TEMP_ECO_NUIT_HIVER}°C. Le chauffage des pièces de vie est éteint.`;\n    } else if (safeRapportModeTemp === 'Ete_Confort' || safeRapportModeTemp === 'Ete Confort') {\n        const tempConfortAffichee = (safeRapportAjustementTemp !== \"Aucun\" && TEMP_CONFORT_ETE_AJUSTEE !== null) ? TEMP_CONFORT_ETE_AJUSTEE : TEMP_CONFORT_ETE;\n        const ajustementText = (safeRapportAjustementTemp !== \"Aucun\") ? ` (ajustée via ${safeRapportAjustementTemp.replace(/_/g, ' ')})` : '';\n        payload.rapportChauffage = `Toutes les pièces sont climatisées autour de ${tempConfortAffichee.toFixed(1)}°C${ajustementText}.`;\n    } else {\n        if (!payload.rapportChauffage) {\n            payload.rapportChauffage = \"Le chauffage et la climatisation sont désactivés.\";\n        }\n    }\n  \n    // --- LOGIQUE RAPPORT VOITURE ---\n    const isChargingCommandSent = msg.commands.includes('CHARGE_VOITURE_ON');\n    if (VOITURE_CHARGE_FORCEE && isChargingCommandSent) {\n        payload.rapportVoiture = \"Charge forcée manuelle en cours.\";\n    } else if (NIVEAU_BATTERIE_VOITURE === -1) {\n        payload.rapportVoiture = \"Voiture non connectée.\";\n    } else if (NIVEAU_BATTERIE_VOITURE >= CONSIGNE_CHARGE_VOITURE) {\n        payload.rapportVoiture = `La batterie a atteint la consigne de ${CONSIGNE_CHARGE_VOITURE}%, la charge est arrêtée.`;\n    } else if (isChargingCommandSent) {\n        payload.rapportVoiture = `Charge automatique en cours pour atteindre ${CONSIGNE_CHARGE_VOITURE}%.`;\n    } else {\n        payload.rapportVoiture = `Charge en attente d'une opportunité (heures creuses/solaire) pour atteindre ${CONSIGNE_CHARGE_VOITURE}%.`;\n    }\n  \n    // --- LOGIQUE RAPPORT PISCINE ---\n    payload.rapportPiscine = msg.commands.includes('MODE_AUTO_PISCINE_ON') ? \"Le mode automatique est activé.\" : \"Le mode automatique est désactivé.\";\n  \n    // --- LOGIQUE RAPPORT BATTERIE ---\n    if (GESTION_AUTO_BATTERIE) {\n        payload.rapportBatterieLigne1 = `La batterie est en mode ${rapportActionBatterie.replace(/_/g, ' ')} (actuellement à ${NIVEAU_BATTERIE_MAISON}%).`;\n        payload.rapportBatterieLigne2 = \"\";\n    } else {\n        payload.rapportBatterieLigne1 = `La gestion de la batterie est en mode forcé : ${rapportActionBatterie.replace(/_/g, ' ')} (actuellement à ${NIVEAU_BATTERIE_MAISON}%).`;\n        payload.rapportBatterieLigne2 = \"\";\n    }\n  \n    // --- AJOUT DE LA SAISON AU RAPPORT ---\n    payload.rapportSaison = `Saison détectée : **${MODE_SAISONNIER.replace(/_/g, ' ')}**`;\n  \n    // Création des deux messages de sortie\n    const msg_actions = { commands: [...new Set(msg.commands)] };\n    const msg_rapport = { payload: payload };\n  \n    return [msg_actions, msg_rapport];\n  \n} catch (e) {\n    node.error(e.stack, msg);\n    return null;\n}\n",
        "outputs": 2,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 580,
        "y": 580,
        "wires": [
            [
                "94560f551005daa5",
                "ecb9bb1ede125540"
            ],
            [
                "1b1d799e4d122d72",
                "de6e300bd1ba9bc8"
            ]
        ]
    },
    {
        "id": "1b1d799e4d122d72",
        "type": "function",
        "z": "236bdcb1284426a5",
        "g": "02df0b2dbe863781",
        "name": "FILTRE RAPPORT QUOTIDIEN",
        "func": "// --- NOUVEAU NOEUD : FILTRE RAPPORT QUOTIDIEN (Version Corrigée) ---\n\n// Accès direct aux variables pour plus de robustesse\nconst HEURE = msg.payload.HEURE;\nconst MINUTE = msg.payload.MINUTE;\nconst HEURE_RAPPORT_QUOTIDIEN = msg.payload.HEURE_RAPPORT_QUOTIDIEN;\nconst MINUTE_RAPPORT_QUOTIDIEN = msg.payload.MINUTE_RAPPORT_QUOTIDIEN;\nconst RAPPORT_DEBUG_MODE = msg.payload.RAPPORT_DEBUG_MODE;\n\nconst today = new Date().toISOString().slice(0, 10);\nconst lastReportDate = flow.get('last_report_date') || '';\nconst reportAlreadySentToday = (today === lastReportDate);\nconst isTimeForDailyReport = (HEURE > HEURE_RAPPORT_QUOTIDIEN || (HEURE === HEURE_RAPPORT_QUOTIDIEN && MINUTE >= MINUTE_RAPPORT_QUOTIDIEN));\n\n// La logique reste la même, mais elle fonctionnera car la variable est lue correctement\nif (RAPPORT_DEBUG_MODE === true || (!reportAlreadySentToday && isTimeForDailyReport)) {\n    if (!RAPPORT_DEBUG_MODE) {\n        flow.set('last_report_date', today);\n    }\n    return msg; // On laisse passer le message\n}\n\n// Sinon, on bloque le message\nreturn null;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 570,
        "y": 640,
        "wires": [
            [
                "e386284bf0cbfb88",
                "3f6bfeaf6b163b71"
            ]
        ]
    },
    {
        "id": "b90c93de46a3ed3b",
        "type": "ha-api",
        "z": "236bdcb1284426a5",
        "g": "4c983f0a668e4a0a",
        "name": "Récupérer Prévisions Météo",
        "server": "bb0a3ead.a33a3",
        "version": 1,
        "debugenabled": false,
        "protocol": "websocket",
        "method": "get",
        "path": "",
        "data": "{\t    \"type\": \"call_service\",\t    \"domain\": \"weather\",\t    \"service\": \"get_forecasts\",\t    \"return_response\": true,\t    \"target\": {\t        \"entity_id\": \"weather.forecast_maison\"\t    },\t    \"service_data\": {\t        \"type\": \"daily\"\t    }\t}",
        "dataType": "jsonata",
        "responseType": "json",
        "outputProperties": [
            {
                "property": "forecast_data",
                "propertyType": "msg",
                "value": "",
                "valueType": "results"
            }
        ],
        "x": 420,
        "y": 80,
        "wires": [
            [
                "7912c6025ff824ef",
                "0b08f44d3510a7fe"
            ]
        ]
    },
    {
        "id": "7912c6025ff824ef",
        "type": "debug",
        "z": "236bdcb1284426a5",
        "g": "4c983f0a668e4a0a",
        "name": "Données api météo",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 450,
        "y": 20,
        "wires": []
    },
    {
        "id": "2ba022c50fc1e42d",
        "type": "catch",
        "z": "236bdcb1284426a5",
        "g": "4c983f0a668e4a0a",
        "name": "",
        "scope": [
            "b90c93de46a3ed3b"
        ],
        "uncaught": false,
        "x": 660,
        "y": 20,
        "wires": [
            [
                "0b08f44d3510a7fe"
            ]
        ]
    },
    {
        "id": "0b08f44d3510a7fe",
        "type": "function",
        "z": "236bdcb1284426a5",
        "g": "4c983f0a668e4a0a",
        "name": "3. Valider Météo et Continuer",
        "func": "// ### CODE DU NŒUD : VALIDATION MÉTÉO (Fonction) - AMÉLIORÉ ###\n\ntry {\n    // Ce noeud reçoit le résultat de l'API météo.\n    // D'après les logs, les données de prévision sont sous msg.forecast_data.response.weather.forecast_maison.forecast\n\n    let weatherForecast = {}; // Initialise un objet vide par défaut\n\n    // Vérifie si msg.forecast_data.response et les données de prévision sont présentes\n    if (msg.forecast_data && msg.forecast_data.response && msg.forecast_data.response['weather.forecast_maison'] && msg.forecast_data.response['weather.forecast_maison'].forecast) {\n        const forecast = msg.forecast_data.response['weather.forecast_maison'].forecast;\n        \n        // AMÉLIORATION : Validation du format des prévisions (au moins 2 jours nécessaires)\n        if (Array.isArray(forecast) && forecast.length >= 2) {\n            weatherForecast = msg.forecast_data.response['weather.forecast_maison'];\n        } else {\n            node.warn(`Format de prévisions météo invalide : ${forecast ? forecast.length : 0} jours disponibles (minimum 2 requis).`);\n        }\n    } else {\n        node.warn(\"L'appel API météo a échoué ou a expiré. Continuation avec des données météo vides.\");\n    }\n\n    // Place les données de prévision (ou l'objet vide) dans msg.forecast_data\n    msg.forecast_data = weatherForecast;\n\n    // Supprime les propriétés originales de l'API pour nettoyer le message\n    delete msg.payload;\n    delete msg.response;\n    delete msg.topic;\n\n    return msg;\n\n} catch (e) {\n    node.error(\"Erreur lors de la validation des données météo : \" + e.message, msg);\n    // Retourne un message avec des données météo vides pour permettre au flux de continuer\n    msg.forecast_data = {};\n    delete msg.payload;\n    delete msg.response;\n    delete msg.topic;\n    return msg;\n}",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 760,
        "y": 80,
        "wires": [
            [
                "e91a1b1b.16e5e8"
            ]
        ]
    },
    {
        "id": "aa0f7df911da2d81",
        "type": "function",
        "z": "236bdcb1284426a5",
        "g": "fcc50eded02e8fc9",
        "name": "Dispatcher Input Number",
        "func": "// ### CODE DU NŒUD : DISPATCHER INPUT NUMBER - AMÉLIORÉ ###\n\ntry {\n    const command = msg.payload;\n    \n    if (!command || typeof command !== 'string') {\n        node.warn(\"Commande invalide reçue par le dispatcher input_number\");\n        return null;\n    }\n    \n    const regex = /^SET_INPUT_NUMBER_([a-zA-Z0-9_]+)_(-?\\d+\\.?\\d*)$/;\n    const match = command.match(regex);\n\n    if (match) {\n        const helperName = match[1];\n        const value = parseFloat(match[2]);\n        \n        if (isNaN(value)) {\n            node.warn(`Valeur numérique invalide pour ${helperName}: ${match[2]}`);\n            return null;\n        }\n        \n        const entityId = `input_number.${helperName}`;\n\n        msg.payload = {\n            type: \"call_service\",\n            domain: \"input_number\",\n            service: \"set_value\",\n            service_data: {\n                value: value\n            },\n            target: {\n                entity_id: entityId\n            }\n        };\n        return msg;\n    }\n\n    return null;\n\n} catch (e) {\n    node.error(\"Erreur dans le dispatcher input_number : \" + e.message, msg);\n    return null;\n}",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1650,
        "y": 280,
        "wires": [
            [
                "14694fc768b31de9"
            ]
        ]
    },
    {
        "id": "43cb5744b2100367",
        "type": "function",
        "z": "236bdcb1284426a5",
        "g": "fcc50eded02e8fc9",
        "name": "Dispatcher Timestamps",
        "func": "// ### CODE DU NŒUD : DISPATCHER TIMESTAMPS - AMÉLIORÉ ###\n\ntry {\n    const command = msg.payload;\n    \n    if (!command || typeof command !== 'string') {\n        node.warn(\"Commande invalide reçue par le dispatcher timestamps\");\n        return null;\n    }\n    \n    const regex = /^SET_INPUT_TEXT_(override_timestamp_[a-zA-Z0-9_]+)_(.*)$/;\n    const match = command.match(regex);\n\n    if (match) {\n        const helperName = match[1];\n        const value = match[2];\n        const entityId = `input_text.${helperName}`;\n        msg.payload = {\n            type: \"call_service\",\n            domain: \"input_text\",\n            service: \"set_value\",\n            service_data: {\n                value: value\n            },\n            target: {\n                entity_id: entityId\n            }\n        };\n        return msg;\n    }\n\n    return null;\n\n} catch (e) {\n    node.error(\"Erreur dans le dispatcher timestamps : \" + e.message, msg);\n    return null;\n}",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1650,
        "y": 240,
        "wires": [
            [
                "14694fc768b31de9"
            ]
        ]
    },
    {
        "id": "cb766a8c136712c2",
        "type": "switch",
        "z": "236bdcb1284426a5",
        "name": "",
        "property": "payload",
        "propertyType": "msg",
        "rules": [
            {
                "t": "regex",
                "v": "^THERMOSTAT_.*_(ON|OFF)$|^[A-Z_]+_TEMP_|^[A-Z_]+_MODE_",
                "vt": "str",
                "case": false
            },
            {
                "t": "regex",
                "v": "^SET_INPUT_TEXT_override_timestamp_",
                "vt": "str",
                "case": false
            },
            {
                "t": "regex",
                "v": "^SET_INPUT_NUMBER_",
                "vt": "str",
                "case": false
            },
            {
                "t": "regex",
                "v": "^SET_INPUT_TEXT_",
                "vt": "str",
                "case": false
            },
            {
                "t": "regex",
                "v": "^(TURN_ON|TURN_OFF)_input_boolean\\.[a-zA-Z0-9_]+$",
                "vt": "str",
                "case": false
            },
            {
                "t": "else"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 6,
        "x": 1340,
        "y": 280,
        "wires": [
            [
                "177b6a4071aa1684"
            ],
            [
                "43cb5744b2100367"
            ],
            [
                "aa0f7df911da2d81"
            ],
            [
                "7574af6c1288c89c"
            ],
            [
                "36e1491f3c52a3f0"
            ],
            [
                "c0b22a0a.a2f648"
            ]
        ]
    },
   

 {
        "id": "216b5ec1cb5975f0",
        "type": "function",
        "z": "236bdcb1284426a5",
        "g": "babe010276d2c37d",
        "name": "FORMATAGE RAPPORT FINAL",
        "func": "// ============================================================\n// NŒUD : FORMATAGE DU RAPPORT FINAL - VERSION V8\n// Type: Function\n// ============================================================\n// Ce nœud génère le rapport final en Markdown avec toutes\n// les informations de la journée.\n//\n// ✅ NOUVEAUTÉS V8 :\n// - Affichage du fournisseur d'électricité actif\n// ============================================================\n\ntry {\n    const payload = msg.payload;\n\n    // Validation des données essentielles\n    if (!payload || typeof payload !== 'object') {\n        throw new Error(\"Payload invalide ou manquant\");\n    }\n\n    let report = \"\"; \n    report += `${payload.rapportSalutation || 'Bonjour !'}\\n`; \n    report += `${payload.rapportMeteoActuelle || 'Informations météo non disponibles.'}\\n\\n`; \n    \n    // ✅ V8 : Affichage du fournisseur\n    if (payload.rapportFournisseur) {\n        report += `${payload.rapportFournisseur}\\n`;\n    }\n    \n    report += `**${payload.rapportTitre || 'Rapport quotidien'}**\\n`;\n    report += `${payload.rapportSaison || ''}\\n`;\n    \n    // Rapport de présence\n    if (payload.rapportPresence) {\n        report += `\\n${payload.rapportPresence}\\n\\n`;\n    } else {\n        report += '\\n';\n    }\n\n    // ✅ V8 : Alertes mode sécurité (affichées en priorité)\n    if (payload.rapportAlertes) { \n        report += `\\n---\\n\\n${payload.rapportAlertes}\\n---\\n\\n`; \n    }\n\n    if (payload.rapportContexte) { \n        report += `${payload.rapportContexte}\\n\\n`; \n    }\n\n    report += `**🌡️ Chauffage**\\n${payload.rapportChauffage || 'Information non disponible'}\\n\\n`; \n    report += `**🚗 Voiture Électrique**\\n${payload.rapportVoiture || 'Information non disponible'}\\n\\n`; \n    report += `**🏊 Piscine**\\n${payload.rapportPiscine || 'Information non disponible'}\\n\\n`; \n    report += `**🔋 Batterie Maison**\\n${payload.rapportBatterieLigne1 || 'Information non disponible'}\\n`; \n    if (payload.rapportBatterieLigne2) { \n        report += `${payload.rapportBatterieLigne2}\\n`; \n    }\n\n    if (msg.quote && msg.quote.citation) {\n        report += `\\n**✨ La pensée du jour...**\\n`;\n        report += `\"${msg.quote.citation}\"`;\n        if (msg.quote.infos && msg.quote.infos.personnage) {\n            report += `\\n${msg.quote.infos.personnage}`;\n        }\n    }\n\n    // 19 phrases de conclusion\n    const conclusions = [\n        \"Passez une excellente journée !\",\n        \"Votre maison veille sur vous.\",\n        \"Toute l'équipe domotique vous souhaite une bonne journée.\",\n        \"Optimisez votre confort, votre maison s'en occupe.\",\n        \"Une domotique performante pour une vie simplifiée.\",\n        \"Profitez de votre journée, votre système intelligent gère le reste.\",\n        \"Économisez l'énergie, votre maison pense à tout.\",\n        \"La maison intelligente travaille pour votre bien-être.\",\n        \"Votre écosystème domotique est à votre service.\",\n        \"Confort optimal garanti par votre système automatisé.\",\n        \"Laissez la technologie simplifier votre quotidien.\",\n        \"Votre maison connectée anticipe vos besoins.\",\n        \"L'intelligence artificielle au service de votre confort.\",\n        \"Détendez-vous, votre habitat intelligent vous facilite la vie.\",\n        \"Une gestion énergétique optimale pour des économies durables.\",\n        \"Votre maison s'adapte à vos habitudes pour un confort sur mesure.\",\n        \"Technologie et confort se rejoignent pour votre bien-être.\",\n        \"Profitez d'une maison qui pense avant vous.\",\n        \"L'avenir de l'habitat commence aujourd'hui chez vous.\"\n    ];\n    report += \"\\n\\n---\\n\" + conclusions[Math.floor(Math.random() * conclusions.length)];\n\n    msg.payload = report;\n    return msg;\n\n} catch (e) {\n    node.error(\"Erreur lors du formatage du rapport : \" + e.message, msg);\n    msg.payload = \"⚠️ Une erreur s'est produite lors de la génération du rapport quotidien.\";\n    return msg;\n}\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 570,
        "y": 1200,
        "wires": [
            [
                "c05179d91f2422d0",
                "5363813865cf8784"
            ]
        ]
    },
    {
        "id": "8aa1c4b133e389fe",
        "type": "function",
        "z": "236bdcb1284426a5",
        "g": "babe010276d2c37d",
        "name": "Extracteur citations Figaro",
        "func": "// Nécessite la bibliothèque 'cheerio' pour analyser le HTML.\nconst cheerio = global.get('cheerio');\nif (!cheerio) {\n    node.error(\"La bibliothèque Cheerio n'est pas disponible. Veuillez vérifier sa configuration dans settings.js.\", msg);\n    msg.payload = [];\n    return msg;\n}\n\nconst html = msg.payload;\nconst $ = cheerio.load(html);\n\nconst citationsList = [];\n// Pas besoin de debugCounter ici, on peut l'enlever ou le laisser si utile pour d'autres diagnostics.\n// let debugCounter = 0; \n\n// Trouver tous les liens de citation\n$('a[href*=\"/citation/\"]').each((i, citationLinkElement) => {\n    // debugCounter++;\n    const $citationLink = $(citationLinkElement);\n    const $li = $citationLink.closest('li');\n\n    // Vérifie si un <li> parent a été trouvé et si le lien de citation n'est pas vide (pour filtrer les faux positifs comme la pagination)\n    if ($li.length && $citationLink.text().trim().length > 0 && !$citationLink.parent().hasClass('pager') && !$li.hasClass('pager')) { // Ajout: ignore les liens de pagination\n        let citationText = $citationLink.text().trim();\n        \n        // Nettoyage de la citation\n        if (citationText.startsWith('“') && citationText.endsWith('”')) {\n            citationText = citationText.substring(1, citationText.length - 1).trim();\n        }\n        if (citationText.startsWith('-')) {\n            citationText = citationText.substring(1).trim();\n        }\n\n        let auteurName = '';\n        \n        // node.warn(`--- CITATION #${debugCounter} (Potentielle): \"${citationText.substring(0, 70)}...\"`);\n\n        // MISE À JOUR ICI : Priorité 1: Trouver l'auteur via un lien biographique AVEC TEXTE\n        const authorLinkWithText = $li.find('a[href*=\"/celebre/biographie/\"]').filter(function() {\n            return $(this).text().trim().length > 0; // Filtre les liens qui ont du texte\n        }).first(); // Prend le premier de ceux qui ont du texte\n\n        if (authorLinkWithText.length) {\n            auteurName = authorLinkWithText.text().trim();\n            // node.warn(`  AUTEUR TROUVÉ (lien biographie AVEC TEXTE): \"${auteurName}\"`);\n        } else {\n            // Priorité 2: Si pas de lien biographique avec texte, chercher \"De [Auteur]\" dans le texte brut\n            const tempLi = $li.clone();\n            // Supprime le lien de la citation pour ne pas qu'il interfère avec l'extraction de l'auteur\n            tempLi.children('a[href*=\"/citation/\"]').first().remove(); \n            // Supprime les images qui pourraient masquer le texte de l'auteur\n            tempLi.find('img').remove();          \n\n            const remainingText = tempLi.text().trim();\n            // node.warn(`  Auteur non trouvé via lien. Texte restant dans LI: \"${remainingText.substring(0, 70)}...\"`);\n            \n            const authorMatch = remainingText.match(/De\\s+([a-zA-ZÀ-ÿ\\s'-]+(?:[\\s-][a-zA-ZÀ-ÿ\\s'-]+)*)/);\n            if (authorMatch && authorMatch[1].trim()) {\n                auteurName = authorMatch[1].trim();\n                // node.warn(`  AUTEUR TROUVÉ (texte brut): \"${auteurName}\"`);\n            } else {\n                // node.warn(`  AUTEUR NON TROUVÉ (texte brut).`);\n            }\n        }\n        \n        if (citationText && auteurName) {\n            citationsList.push({ citation: citationText, auteur: auteurName });\n            // node.warn(`  --> CITATION & AUTEUR AJOUTÉS.`);\n        } else {\n            // node.warn(`  --> CITATION & AUTEUR NON AJOUTÉS (manque citation: ${!citationText}, manque auteur: ${!auteurName}).`);\n        }\n    } // else { node.warn(`--- CITATION #${debugCounter}: LIEN IGNORÉ (pas de parent LI, lien vide, ou pagination).`); }\n});\n\nif (citationsList.length > 0) {\n    msg.payload = citationsList;\n    node.status({fill:\"green\", shape:\"dot\", text:`${citationsList.length} citations trouvées`});\n} else {\n    msg.payload = [];\n    node.status({fill:\"red\", shape:\"dot\", text:\"Aucune citation trouvée sur la page Evene.\"});\n    node.warn(\"FINAL: Impossible de trouver des citations ou des auteurs (liés) sur la page Evene.\");\n}\n\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 570,
        "y": 1000,
        "wires": [
            [
                "2496a518e5e0c785",
                "772946bda12f4bc4"
            ]
        ]
    },
    {
        "id": "773615832d306ad7",
        "type": "http request",
        "z": "236bdcb1284426a5",
        "g": "babe010276d2c37d",
        "name": "",
        "method": "GET",
        "ret": "txt",
        "paytoqs": "ignore",
        "url": "http://evene.lefigaro.fr/citations/mot.php?mot=chaque-jour ",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "",
        "senderr": false,
        "headers": [],
        "x": 570,
        "y": 940,
        "wires": [
            [
                "8aa1c4b133e389fe",
                "2d0a7c6e9897779f"
            ]
        ]
    },
    {
        "id": "2496a518e5e0c785",
        "type": "function",
        "z": "236bdcb1284426a5",
        "g": "babe010276d2c37d",
        "name": "Prendre une citation au hasard",
        "func": "// Ce nœud prend un tableau de citations en entrée et en sélectionne une au hasard.\n\nconst citations = msg.payload;\n\nif (Array.isArray(citations) && citations.length > 0) {\n    // Génère un index aléatoire\n    const randomIndex = Math.floor(Math.random() * citations.length);\n    \n    // Sélectionne la citation aléatoire\n    msg.payload = citations[randomIndex];\n    \n    node.status({fill:\"green\", shape:\"dot\", text:\"1 citation aléatoire sélectionnée\"});\n} else {\n    msg.payload = { citation: \"Aucune citation trouvée.\", auteur: \"\" }; // Message par défaut\n    node.status({fill:\"red\", shape:\"dot\", text:\"Aucune citation disponible\"});\n    node.warn(\"Le tableau de citations est vide ou invalide. Impossible de sélectionner une citation aléatoire.\");\n}\n\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 570,
        "y": 1060,
        "wires": [
            [
                "43b9f87012cfcd5a",
                "4cdd0a0b141d9ec5"
            ]
        ]
    },
    {
        "id": "e386284bf0cbfb88",
        "type": "change",
        "z": "236bdcb1284426a5",
        "g": "babe010276d2c37d",
        "name": "Sauvegarder Payload Principal",
        "rules": [
            {
                "t": "move",
                "p": "payload",
                "pt": "msg",
                "to": "originalPayload",
                "tot": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 570,
        "y": 820,
        "wires": [
            [
                "c8d20c26f125d64c",
                "37cb0830a57eb629"
            ]
        ]
    },
    {
        "id": "43b9f87012cfcd5a",
        "type": "change",
        "z": "236bdcb1284426a5",
        "g": "babe010276d2c37d",
        "name": "Formater Citation & Restaurer Payload",
        "rules": [
            {
                "t": "set",
                "p": "quote",
                "pt": "msg",
                "to": "{\t    \"citation\": payload.citation,\t    \"infos\": {\t        \"personnage\": payload.auteur\t    }\t}",
                "tot": "jsonata"
            },
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "originalPayload",
                "tot": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 570,
        "y": 1140,
        "wires": [
            [
                "216b5ec1cb5975f0",
                "98c69aae6314322e"
            ]
        ]
    },
    {
        "id": "aad154b7d41f8ef1",
        "type": "inject",
        "z": "236bdcb1284426a5",
        "g": "60a43b19dc469992",
        "name": "Standby",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 2960,
        "y": 740,
        "wires": [
            [
                "701e199ff346c6ac"
            ]
        ]
    },
    {
        "id": "f138917af391766c",
        "type": "inject",
        "z": "236bdcb1284426a5",
        "g": "60a43b19dc469992",
        "name": "Discharge",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 2960,
        "y": 680,
        "wires": [
            [
                "792c83c59b62bdfc"
            ]
        ]
    },
    {
        "id": "dfcb40e00213e5a7",
        "type": "inject",
        "z": "236bdcb1284426a5",
        "g": "60a43b19dc469992",
        "name": "Charge",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 2950,
        "y": 560,
        "wires": [
            [
                "0440734ffb117e0b"
            ]
        ]
    },
    {
        "id": "c0e73fed01db4c82",
        "type": "inject",
        "z": "236bdcb1284426a5",
        "g": "60a43b19dc469992",
        "name": "Auto",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 2950,
        "y": 500,
        "wires": [
            [
                "6ef2e69c91c99492"
            ]
        ]
    },
    {
        "id": "601203dc5389b7f7",
        "type": "ha-button",
        "z": "236bdcb1284426a5",
        "g": "4c983f0a668e4a0a",
        "name": "Start GEMINI gestion",
        "version": 0,
        "debugenabled": false,
        "outputs": 1,
        "entityConfig": "29ec8754ac241cdd",
        "outputProperties": [
            {
                "property": "payload",
                "propertyType": "msg",
                "value": "string",
                "valueType": "entityState"
            },
            {
                "property": "topic",
                "propertyType": "msg",
                "value": "",
                "valueType": "triggerId"
            },
            {
                "property": "data",
                "propertyType": "msg",
                "value": "",
                "valueType": "entity"
            }
        ],
        "x": 120,
        "y": 20,
        "wires": [
            [
                "b90c93de46a3ed3b"
            ]
        ]
    },
    {
        "id": "17651a22e07cec93",
        "type": "debug",
        "z": "236bdcb1284426a5",
        "g": "4c983f0a668e4a0a",
        "name": "debug variable de contexte",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 1080,
        "y": 140,
        "wires": []
    },
    {
        "id": "12a9b25981eba61a",
        "type": "api-current-state",
        "z": "236bdcb1284426a5",
        "g": "0ae09618a71e329f",
        "name": "Batterie.Maison",
        "server": "bb0a3ead.a33a3",
        "version": 3,
        "outputs": 1,
        "halt_if": "",
        "halt_if_type": "str",
        "halt_if_compare": "is",
        "entity_id": "input_select.batterie_maison_mode_force",
        "state_type": "str",
        "blockInputOverrides": true,
        "outputProperties": [
            {
                "property": "payload",
                "propertyType": "msg",
                "value": "string",
                "valueType": "entityState"
            },
            {
                "property": "data",
                "propertyType": "msg",
                "value": "",
                "valueType": "entity"
            }
        ],
        "for": "0",
        "forType": "num",
        "forUnits": "minutes",
        "override_topic": false,
        "state_location": "payload",
        "override_payload": "msg",
        "entity_location": "data",
        "override_data": "msg",
        "x": 2220,
        "y": 940,
        "wires": [
            [
                "1519d53bfdfd6107"
            ]
        ]
    },
    {
        "id": "1519d53bfdfd6107",
        "type": "switch",
        "z": "236bdcb1284426a5",
        "g": "0ae09618a71e329f",
        "name": "",
        "property": "payload",
        "propertyType": "msg",
        "rules": [
            {
                "t": "eq",
                "v": "Auto",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "Charge",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "Equalization",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "Discharge",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "Standby",
                "vt": "str"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 5,
        "x": 2120,
        "y": 880,
        "wires": [
            [
                "6ef2e69c91c99492"
            ],
            [
                "0440734ffb117e0b"
            ],
            [
                "ca2320cc9cfc5f28"
            ],
            [
                "792c83c59b62bdfc"
            ],
            [
                "701e199ff346c6ac"
            ]
        ]
    },
    {
        "id": "13080791b21a5c39",
        "type": "server-state-changed",
        "z": "236bdcb1284426a5",
        "g": "0ae09618a71e329f",
        "name": "State.battery",
        "server": "bb0a3ead.a33a3",
        "version": 6,
        "outputs": 1,
        "exposeAsEntityConfig": "",
        "entities": {
            "entity": [
                "input_select.batterie_maison_mode_force"
            ],
            "substring": [],
            "regex": []
        },
        "outputInitially": false,
        "stateType": "str",
        "ifState": "",
        "ifStateType": "str",
        "ifStateOperator": "is",
        "outputOnlyOnStateChange": true,
        "for": "0",
        "forType": "num",
        "forUnits": "minutes",
        "ignorePrevStateNull": false,
        "ignorePrevStateUnknown": false,
        "ignorePrevStateUnavailable": false,
        "ignoreCurrentStateUnknown": false,
        "ignoreCurrentStateUnavailable": false,
        "outputProperties": [
            {
                "property": "payload",
                "propertyType": "msg",
                "value": "string",
                "valueType": "entityState"
            },
            {
                "property": "data",
                "propertyType": "msg",
                "value": "",
                "valueType": "eventData"
            },
            {
                "property": "topic",
                "propertyType": "msg",
                "value": "",
                "valueType": "triggerId"
            }
        ],
        "x": 1950,
        "y": 940,
        "wires": [
            [
                "12a9b25981eba61a"
            ]
        ]
    },
    {
        "id": "9b50dd24a4eed72a",
        "type": "debug",
        "z": "236bdcb1284426a5",
        "name": "logique batterie",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 180,
        "y": 520,
        "wires": []
    },
    {
        "id": "ca2320cc9cfc5f28",
        "type": "change",
        "z": "236bdcb1284426a5",
        "g": "60a43b19dc469992",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "300",
                "tot": "str"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 2450,
        "y": 620,
        "wires": [
            [
                "bc5f6b2053564699"
            ]
        ]
    },
    {
        "id": "bc5f6b2053564699",
        "type": "mqtt out",
        "z": "236bdcb1284426a5",
        "g": "60a43b19dc469992",
        "name": "INVERTER_EQUALIZATION_300",
        "topic": "Sofar2mqtt/set/charge",
        "qos": "",
        "retain": "",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "3348075e0f83efab",
        "x": 2760,
        "y": 620,
        "wires": []
    },
    {
        "id": "3a6c83d3dfce5c85",
        "type": "inject",
        "z": "236bdcb1284426a5",
        "g": "60a43b19dc469992",
        "name": "Charge",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 2950,
        "y": 620,
        "wires": [
            [
                "ca2320cc9cfc5f28"
            ]
        ]
    },
    {
        "id": "36e1491f3c52a3f0",
        "type": "function",
        "z": "236bdcb1284426a5",
        "g": "fcc50eded02e8fc9",
        "name": "Dispatcher voiture",
        "func": "// ### CODE DU NŒUD : DISPATCHER VOITURE - AMÉLIORÉ ###\n\ntry {\n    const command = msg.payload;\n    \n    if (!command || typeof command !== 'string') {\n        node.warn(\"Commande invalide reçue par le dispatcher voiture\");\n        return null;\n    }\n    \n    const regex = /^(TURN_ON|TURN_OFF)_(input_boolean\\.[a-zA-Z0-9_]+)$/;\n    const match = command.match(regex);\n\n    if (match) {\n        const service = match[1].toLowerCase(); // 'turn_on' or 'turn_off'\n        const entityId = match[2];\n\n        msg.payload = {\n            type: \"call_service\",\n            domain: \"input_boolean\",\n            service: service,\n            target: {\n                entity_id: entityId\n            }\n        };\n        return msg;\n    }\n\n    return null;\n\n} catch (e) {\n    node.error(\"Erreur dans le dispatcher voiture : \" + e.message, msg);\n    return null;\n}",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1650,
        "y": 360,
        "wires": [
            [
                "14694fc768b31de9"
            ]
        ]
    },
    {
        "id": "94efe42faad5ce9c",
        "type": "debug",
        "z": "236bdcb1284426a5",
        "name": "logique vehicule",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 180,
        "y": 460,
        "wires": []
    },
    {
        "id": "48b93facf3bde2b0",
        "type": "debug",
        "z": "236bdcb1284426a5",
        "name": "logique thermostat",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 150,
        "y": 400,
        "wires": []
    },
    {
        "id": "ecb9bb1ede125540",
        "type": "debug",
        "z": "236bdcb1284426a5",
        "name": "Preparation rapport et finalisation sortie 1",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 180,
        "y": 560,
        "wires": []
    },
    {
        "id": "3f6bfeaf6b163b71",
        "type": "debug",
        "z": "236bdcb1284426a5",
        "name": "Filtre rapport quotidien",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 140,
        "y": 640,
        "wires": []
    },
    {
        "id": "c8d20c26f125d64c",
        "type": "debug",
        "z": "236bdcb1284426a5",
        "name": "Sauvegarder Payload principal",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 950,
        "y": 820,
        "wires": []
    },
    {
        "id": "2d0a7c6e9897779f",
        "type": "debug",
        "z": "236bdcb1284426a5",
        "name": "Requete Http",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 890,
        "y": 940,
        "wires": []
    },
    {
        "id": "772946bda12f4bc4",
        "type": "debug",
        "z": "236bdcb1284426a5",
        "name": "Extraction citations",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 910,
        "y": 1000,
        "wires": []
    },
    {
        "id": "4cdd0a0b141d9ec5",
        "type": "debug",
        "z": "236bdcb1284426a5",
        "name": "Prendre une citation au hasard",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 950,
        "y": 1060,
        "wires": []
    },
    {
        "id": "98c69aae6314322e",
        "type": "debug",
        "z": "236bdcb1284426a5",
        "name": "Formater citation & Restaurer Payload",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 970,
        "y": 1140,
        "wires": []
    },
    {
        "id": "5363813865cf8784",
        "type": "debug",
        "z": "236bdcb1284426a5",
        "name": "Formatage rapport final",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 930,
        "y": 1280,
        "wires": []
    },
    {
        "id": "de6e300bd1ba9bc8",
        "type": "debug",
        "z": "236bdcb1284426a5",
        "name": "Preparation rapport et finalisation sortie 2",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 180,
        "y": 600,
        "wires": []
    },
    {
        "id": "37cb0830a57eb629",
        "type": "function",
        "z": "236bdcb1284426a5",
        "g": "babe010276d2c37d",
        "name": "GÉNÉRATION URL ALÉATOIRE",
        "func": "// ### CODE DU NŒUD : GÉNÉRATION URL PAGE ALÉATOIRE EVENE - SANS DOUBLON JOURNALIER ###\n\n// Configuration\nconst baseUrl = \"http://evene.lefigaro.fr/citations/mot.php?mot=chaque-jour\";\nconst nombrePagesMax = 50; // Ajustez selon le nombre réel de pages disponibles\n\n// Récupérer la date du jour (format YYYY-MM-DD)\nconst today = new Date().toISOString().split('T')[0];\n\n// Récupérer l'historique des pages utilisées aujourd'hui\nlet pagesUtilisees = flow.get('evene_pages_utilisees') || { date: '', pages: [] };\n\n// Si on change de jour, réinitialiser l'historique\nif (pagesUtilisees.date !== today) {\n    pagesUtilisees = { date: today, pages: [] };\n    flow.set('evene_pages_utilisees', pagesUtilisees);\n    node.warn(`Nouveau jour détecté (${today}). Historique des pages réinitialisé.`);\n}\n\n// Si toutes les pages ont été utilisées aujourd'hui, réinitialiser\nif (pagesUtilisees.pages.length >= nombrePagesMax) {\n    node.warn(`Toutes les ${nombrePagesMax} pages ont été utilisées aujourd'hui. Réinitialisation de l'historique.`);\n    pagesUtilisees.pages = [];\n}\n\n// Générer une page aléatoire non utilisée\nlet pageAleatoire;\nlet tentatives = 0;\nconst maxTentatives = 50; // Sécurité anti-boucle infinie\n\ndo {\n    pageAleatoire = Math.floor(Math.random() * nombrePagesMax) + 1;\n    tentatives++;\n\n    if (tentatives > maxTentatives) {\n        node.warn(`Impossible de trouver une page non utilisée après ${maxTentatives} tentatives. Réinitialisation forcée.`);\n        pagesUtilisees.pages = [];\n        pageAleatoire = Math.floor(Math.random() * nombrePagesMax) + 1;\n        break;\n    }\n} while (pagesUtilisees.pages.includes(pageAleatoire));\n\n// Ajouter cette page à l'historique\npagesUtilisees.pages.push(pageAleatoire);\nflow.set('evene_pages_utilisees', pagesUtilisees);\n\n// Construire l'URL\nlet url = baseUrl;\nif (pageAleatoire > 1) {\n    url += `&p=${pageAleatoire}`;\n}\n\nmsg.url = url;\nmsg.pageNumber = pageAleatoire;\n\nnode.status({\n    fill: \"blue\",\n    shape: \"dot\",\n    text: `Page ${pageAleatoire}/${nombrePagesMax} (${pagesUtilisees.pages.length} utilisées aujourd'hui)`\n});\n\nnode.warn(`📖 Page ${pageAleatoire} sélectionnée (${pagesUtilisees.pages.length}/${nombrePagesMax} pages déjà utilisées aujourd'hui)`);\n\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 560,
        "y": 880,
        "wires": [
            [
                "773615832d306ad7"
            ]
        ]
    },
    {
        "id": "3348075e0f83efab",
        "type": "mqtt-broker",
        "name": "Home Assistant Mosquitto",
        "broker": "localhost",
        "port": "1883",
        "clientid": "HAClient",
        "autoConnect": true,
        "usetls": false,
        "protocolVersion": "4",
        "keepalive": "60",
        "cleansession": true,
        "autoUnsubscribe": true,
        "birthTopic": "",
        "birthQos": "0",
        "birthRetain": "false",
        "birthPayload": "",
        "birthMsg": {},
        "closeTopic": "",
        "closeQos": "0",
        "closePayload": "",
        "closeMsg": {},
        "willTopic": "",
        "willQos": "0",
        "willPayload": "",
        "willMsg": {},
        "userProps": "",
        "sessionExpiry": ""
    },
    {
        "id": "bb0a3ead.a33a3",
        "type": "server",
        "name": "Home Assistant",
        "addon": true
    },
    {
        "id": "514978627925e867",
        "type": "telegrambot-config",
        "botname": "NectiHome_bot",
        "usernames": "",
        "chatIds": "803094914",
        "pollInterval": 300
    },
    {
        "id": "29ec8754ac241cdd",
        "type": "ha-entity-config",
        "server": "bb0a3ead.a33a3",
        "deviceConfig": "",
        "name": "START GEMINI GESTION",
        "version": 6,
        "entityType": "button",
        "haConfig": [
            {
                "property": "name",
                "value": "START GEMINI GESTION"
            },
            {
                "property": "icon",
                "value": ""
            },
            {
                "property": "entity_picture",
                "value": ""
            },
            {
                "property": "entity_category",
                "value": ""
            },
            {
                "property": "device_class",
                "value": ""
            }
        ],
        "resend": false,
        "debugEnabled": false
    },
    {
        "id": "eae929f0a6f222ba",
        "type": "global-config",
        "env": [],
        "modules": {
            "node-red-contrib-cron-plus": "2.2.4",
            "node-red-contrib-home-assistant-websocket": "0.80.3",
            "@danielnguyen/node-red-contrib-telegrambot-home": "0.5.3"
        }
    }
]

Heureusement que tu l’avais précisé :rofl:

oui c’est vrai lol, après j’ai appris deux, trois trucs du coup :sweat_smile:

Ce sujet a été automatiquement fermé après 60 jours. Aucune réponse n’est permise dorénavant.