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 :
- Une analyse météo simplifiée pour obtenir la température maximale prévue.
- 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)
- Récupération Météo : Un premier appel à Gemini récupère les prévisions météo de base (température max).
- 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.
- 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.
- 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.
- 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).
- 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 :
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"
}
}
]