Contents
- 1 Intro
- 2 Objectif du flow
- 3 Le flow Node-RED
- 3.1 Visualisation du flow
- 3.2 Analyse du flow
- 3.2.1 A) Bloc “Estimation de la production PV”
- 3.2.2 B) Bloc “Entrées Home Assistant (forçages et autorisations)”
- 3.2.3 C) Bloc “Horloge CP 22h → 6h”
- 3.2.4 D) Bloc “Calcul principal : SoC cible + commande Victron”
- 3.2.5 E) Bloc “Écriture des settings dans Venus OS”
- 3.2.6 F) Bloc “MQTT sortant pour Home Assistant”
- 3.3 Détails techniques (extraits utiles)
- 4 Dashboard Node red:
- 5 Intégration avec Home Assistant
- 6 Palettes utilisées
- 7 Conclusion
- 8 Annexe JSON
Intro
Depuis plusieurs mois, mon installation photovoltaïque Victron a évolué : nouveaux capteurs, nouveaux besoins (Tempo, consommation nocturne, sécurisation du SoC), et surtout une logique de pilotage plus structurée. Plutôt que de maintenir une série d’articles éparpillés autour de Node-RED et de quelques flows historiques, j’ai décidé de repartir sur une base saine : un article unique, complet, et régulièrement maintenu.
L’objectif de ce billet est simple : présenter mon architecture de gestion énergétique Victron, centrée sur Node-RED (collecte, calculs, décisions) et MQTT/Home Assistant (supervision et commandes), avec un focus sur un point clé : déterminer automatiquement une cible de SoC et piloter intelligemment la charge programmée sur une fenêtre nocturne (typique 22h → 6h). Je détaille ici le pourquoi, le comment, et je fournis les éléments concrets pour reproduire l’approche : structure du flow, variables manipulées, topics MQTT, et points de vigilance.
Objectif du flow
Ce flow, nommé « Calcul Cible SOC pour CP », a 3 missions :
- Estimer la production PV (aujourd’hui / J+1)
- récupération VRM côté onduleur PV (AC-coupled) et côté MPPT
- normalisation (Wh → kWh arrondi)
- somme des prévisions et publication MQTT
- Calculer un SoC cible pour la charge programmée
- formule simple et robuste :
seuil = 120 - 2.5 × prevision_kWh - arrondi au pas de 5%, bornage 0–100%
- possibilité de forçage manuel (ex : CP à X% même si la prévision est bonne)
- formule simple et robuste :
- Piloter CP1 sur Victron (fenêtre 22h–6h)
- activation/désactivation via une autorisation (Home Assistant)
- écriture des paramètres CP1 dans Venus OS : Day, Start, Duration, Soc (et un flag autoconsommation si besoin)
- log synthétique et publication MQTT des valeurs utiles (cible SoC, valid CP, durée, heure début)
Ce flow sert donc à la fois de collecte (prévisions VRM), de traitement (calcul), et de commande (écriture settings Victron) — tout en restant “observable” grâce aux sorties MQTT et au log.
Le flow Node-RED
Visualisation du flow


Analyse du flow
A) Bloc “Estimation de la production PV”
- Deux nœuds VRM API récupèrent les prévisions de production :
- PV inverter yield forecast (AC-coupled)
- PV charger yield forecast (MPPT1)
- Chaque flux passe dans une fonction de normalisation :
- extraction du champ VRM
totals.* - conversion Wh → kWh
- stockage en variables
flow.previmoetflow.previmppt1
- extraction du champ VRM
- Une fonction Somme des deux additionne les deux prévisions et publie :
mp2/dess_remy/prod_onduleurmp2/dess_remy/prod_mppt1mp2/dess_remy/prod_total(QoS 2 + retain = pratique pour HA)
B) Bloc “Entrées Home Assistant (forçages et autorisations)”
Trois topics MQTT (HA → Node-RED) permettent de reprendre la main :
ha/mp2/cp/validcp: autorisation globale de CP (ON/OFF)ha/mp2/cp/forcage100: forçage manuel actif (ON/OFF)ha/mp2/cp/niveauforcagecp1: niveau de SoC cible forcé (ex : 80%)
Ces entrées alimentent des variables flow/global consommées par la fonction principale.
C) Bloc “Horloge CP 22h → 6h”
Un BigTimer définit la fenêtre de charge (22:00 → 06:00) et publie un état simple on/off :
horloge = 1quand on est dans la fenêtrehorloge = 0quand on en sort
Le flow garde ainsi une logique lisible : “CP active seulement si HA autorise ET si horloge=1”.
D) Bloc “Calcul principal : SoC cible + commande Victron”
La fonction V6 fait le cœur du travail :
- Calcul du seuil SoC cible
- formule :
seuil = 120 - 2.5 × prod_kWh - forçage possible si
forcage100 == on:seuil = niveau_forcé - arrondi au pas de 5% et bornage 0–100
- Activation CP1
- si HA autorise ET horloge=1 → activation CP1 (
payload=7sur “Day=Everyday”) - sinon → désactivation (
payload=-1)
- Compatibilité “Start / Duration”
- conversion minutes → secondes, pour alimenter les settings Victron :
/Schedule/Charge/0/Start/Schedule/Charge/0/Duration
- Log synthétique
- un message unique, lisible, qui affiche : SoC actuel, seuil, forçage, durée HC, état horloge, et sortie effective.
E) Bloc “Écriture des settings dans Venus OS”
Les nœuds victron-output-ess écrivent directement dans Venus OS :
- Validation CP (Day) :
/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Day - Heure début :
/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Start - Durée :
/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Duration - Cible SoC :
/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Soc - (option) AllowDischarge / autoconsommation :
/Settings/CGwacs/BatteryLife/Schedule/Charge/0/AllowDischarge
F) Bloc “MQTT sortant pour Home Assistant”
Pour supervision et affichage HA, le flow publie aussi :
mp2/dess_remy/cible_socmp2/multiplus2/valide_cp(converti enon/offvia un mapping)mp2/dess_remy/h_debut(format HH:MM)mp2/dess_remy/duree(format HH:MM)
Détails techniques (extraits utiles)
1) Normalisation VRM → kWh (exemple)
// Extrait la prévision PV (Wh) -> kWh arrondi
const d = msg.payload;
let totalWh = 0;
if (d && d.totals && d.totals.vrm_pv_inverter_yield_fc != null) {
const v = Number(d.totals.vrm_pv_inverter_yield_fc);
if (Number.isFinite(v)) totalWh = v;
}
let totalKWh = Math.round(totalWh / 1000);
msg.payload = totalKWh;
flow.set("previmo", totalKWh);
return msg;
2) Somme des prévisions (MPPT + onduleur)
let p = Number(flow.get('previmo')) || 0;
let c = Number(flow.get('previmppt1')) || 0;
msg.payload = p + c;
return msg;
3) Calcul SoC cible + activation CP (principe)
seuil = 120 - 2.5 × prod- arrondi à 5%
- forçage si besoin
- activation CP seulement si HA autorise + horloge active
Dashboard Node red:

Intégration avec Home Assistant
Le broker MQTT (Home Assistant) reçoit :
- les prévisions
prod_*(très utiles pour tableaux de bord et automations) - la cible SoC retenue
- l’état CP (on/off)
- la durée et l’heure début en format HH:MM
- le log texte (optionnel, très pratique en debug)

On ci-dessous le résultat d’une charge nocturne, la charge programmée démarre vers 1:00 pour se terminer avant 6:00

Côté HA, tu peux :
- créer des
sensorMQTT pourprod_total,cible_soc,duree, etc. - utiliser
input_boolean/input_numberpourvalidcp,forcage100,niveauforcagecp1, et publier vers les topicsha/mp2/cp/....
Fichier victron.yaml permettant de récuperer les mqtt:
mqtt:
sensor:
# MQTT
- name: "MP2 SOC theorique par MQTT"
unique_id: mp2_soc_theorique_par_mqtt"
state_topic: "mp2/batteries/soc_theorique"
unit_of_measurement: '%'
device_class: battery
state_class: measurement
- name: "MP2 VALIDE CP MQTT"
unique_id: mp2_valide_cp_par_mqtt"
state_topic: "mp2/multiplus2/valide_cp"
- name: "Capacité Batterie Estimée par NR"
state_topic: "mp2/batteries/capacity_est_ah"
unit_of_measurement: "Ah"
icon: mdi:battery-charging-outline
state_class: measurement
# DESS Remy
- name: "MP2 Prod VRM TOTAL MQTT"
unique_id: mp2_prod_vrm_total_mqtt"
state_topic: "mp2/dess_remy/prod_total"
unit_of_measurement: 'kW'
device_class: power
state_class: measurement
- name: "MP2 Cible SOC par MQTT"
unique_id: mp2_cible_soc_par_mqtt"
state_topic: "mp2/dess_remy/cible_soc"
unit_of_measurement: '%'
device_class: battery
state_class: measurement
- name: "MP2 DESS Difference Cible-SOC"
unique_id: mp2_difference_cible_soc"
state_topic: "mp2/dess_remy/diff_soc"
unit_of_measurement: '%'
device_class: battery
state_class: measurement
- name: "MP2 DESS H_Debut DESS Remy"
unique_id: mp2_dess_h_debut"
state_topic: "mp2/dess_remy/h_debut"
- name: "MP2 DESS Durée DESS Remy"
unique_id: mp2_dess_duree"
state_topic: "mp2/dess_remy/duree"
Palettes utilisées
Pour reproduire ce flow, installer :
- node-red-contrib-victron (entrées/sorties ESS vers Venus OS)
- palette VRM API (nœud
vrm-apiutilisé pour récupérer les stats/prévisions VRM) - node-red-contrib-bigtimer (horloge 22h → 6h)
- node-red-dashboard (ui_gauge, ui_text, ui_chart)
- node-red-node-mqtt (mqtt in/out)
Conclusion
Avec ce flow, je ne me contente plus de “collecter des données” : j’intègre la prévision (VRM), la décision (calcul cible SoC) et l’action (écriture CP1 dans Victron), tout en gardant une supervision propre via MQTT/Home Assistant.
Annexe JSON
Flux JSON à copier/coller :
[
{
"id": "5fe7408840b1cdf3",
"type": "tab",
"label": "Calcul Cible SOC pour CP",
"disabled": false,
"info": "",
"env": []
},
{
"id": "0af06d70cb632e52",
"type": "change",
"z": "5fe7408840b1cdf3",
"name": "Save SOC",
"rules": [
{
"t": "set",
"p": "soc",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 330,
"y": 1100,
"wires": [
[
"658208f94208bd9f",
"6901655995625596",
"e23bf7ba06f7abf5",
"67bc6865c7406fee"
]
]
},
{
"id": "d0900f1bce2a4111",
"type": "change",
"z": "5fe7408840b1cdf3",
"name": "Save previ_prod",
"rules": [
{
"t": "set",
"p": "previ_prod",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 340,
"y": 1140,
"wires": [
[
"6f92550a55e951b7",
"e23bf7ba06f7abf5",
"9a02956e8c37d773"
]
]
},
{
"id": "6f92550a55e951b7",
"type": "debug",
"z": "5fe7408840b1cdf3",
"name": "debug 27",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "payload",
"targetType": "msg",
"statusVal": "payload",
"statusType": "auto",
"x": 520,
"y": 1140,
"wires": []
},
{
"id": "9cf84313ff660683",
"type": "comment",
"z": "5fe7408840b1cdf3",
"name": "Calcul du niveau SOC pour la charge programmée",
"info": "",
"x": 530,
"y": 440,
"wires": []
},
{
"id": "311d550b1ef83e2d",
"type": "change",
"z": "5fe7408840b1cdf3",
"name": "Horloge",
"rules": [
{
"t": "set",
"p": "horloge",
"pt": "flow",
"to": "payload",
"tot": "msg"
},
{
"t": "set",
"p": "hdebut",
"pt": "flow",
"to": "start",
"tot": "msg"
},
{
"t": "set",
"p": "hfin",
"pt": "flow",
"to": "end",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 320,
"y": 760,
"wires": [
[
"e23bf7ba06f7abf5"
]
]
},
{
"id": "5010cfdb6709a7ec",
"type": "link in",
"z": "5fe7408840b1cdf3",
"name": "In_prod_total",
"links": [
"04f96335d4b31ec0"
],
"x": 115,
"y": 1160,
"wires": [
[
"d0900f1bce2a4111"
]
]
},
{
"id": "4047bb58bd817588",
"type": "mqtt out",
"z": "5fe7408840b1cdf3",
"name": "Cible SOC",
"topic": "mp2/dess_remy/cible_soc",
"qos": "",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "502248144035edbf",
"x": 570,
"y": 900,
"wires": []
},
{
"id": "e23bf7ba06f7abf5",
"type": "function",
"z": "5fe7408840b1cdf3",
"name": "V6",
"func": "// ===================================================================\n// 🔋 DESS v4 simplifié : gestion charge programmée 22h–6h\n// Garde le calcul du SoC théorique, supprime diff/h_corr (sorties 5 et 6)\n// ===================================================================\n\n// === Lecture des variables ===\nlet soc = flow.get('soc'); // SoC actuel (%)\nlet prod = flow.get('previ_prod'); // Prévision de production (kWh)\nlet f100 = flow.get('forc100'); // Forçage manuel (on/off)\nlet nfcp1 = flow.get('niveauforcp1'); // Niveau CP1 forcé\nlet havalid = global.get('valid_cp_ess'); // Autorisation HA (\"on\"/true)\nlet hdeb = flow.get('hdebut'); // Heure début HC (min)\nlet hfin = flow.get('hfin'); // Heure fin HC (min)\nlet horloge = flow.get('horloge'); // 1 = fenêtre 22h–6h\n\n// Normalisation HA_ON\nconst HA_ON = (havalid === \"on\" || havalid === true || havalid === 1);\n\n// === Constantes ===\nconst a = 120; // Base de calcul du SoC cible\nconst b = 2.5; // Pondération selon la prévision\n\n// === Variables locales ===\nlet seuil = 0;\nlet valid = { payload: 7 };\nlet devalid = { payload: -1 };\nlet debug = {};\nlet duree = {};\nlet hDebutS = {};\nlet autoconsoMsg = { payload: 0 }; // 1->PV+Batterie / 0->PV\n\n// ===================================================================\n// 1️⃣ Calcul du SEUIL cible SoC\n// ===================================================================\nseuil = a - (b * prod);\n\n// Si forçage actif et nfcp1 est un nombre valide, on impose le seuil\n// Force au Niveau forcé de charge CP1\nif (f100 == 'on'){\nseuil=nfcp1; \n}\n\nseuil = Math.trunc(Math.round(seuil / 5) * 5);\nif (seuil < 15) seuil = 0;\nif (seuil > 100) seuil = 100;\n//if (seuil > 95) seuil = 95;\n\n\nflow.set(\"seuil_soc\", seuil);\n\n// ===================================================================\n// 2️⃣ Calcul de la durée et heure début HC (pour compatibilité)\n// ===================================================================\nlet h_corr = hdeb / 60;\nlet fin = hfin / 60;\nlet tps = (h_corr > fin)\n ? (24 - h_corr + fin) * 3600\n : (fin - h_corr) * 3600;\n\nduree = { payload: tps };\nhDebutS = { payload: h_corr * 3600 };\n\n// Format durée HH:MM\nlet h = Math.floor(tps / 3600);\nlet m = Math.floor((tps % 3600) / 60);\nlet duree_hhmm = `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}`;\n// ===================================================================\n// 3️⃣ Sortie 1 : Activation charge programmée Victron\n// ===================================================================\nlet actif_payload = -1;\n\nif (HA_ON && horloge === 1) {\n actif_payload = 7; // Active la charge programmée (CP1)\n} else {\n actif_payload = -1; // Mode normal / DESS auto\n}\n\nlet ess_actif = (actif_payload === 7);\nflow.set('ess_actif', ess_actif);\n\n// ===================================================================\n// 4️⃣ Log synthétique\n// ===================================================================\n// Etat forçage lisible\nlet f100_actif = (f100 === 'on');\nlet f100_txt = f100_actif ? \"ON\" : \"OFF\";\nlet nfcp1_txt = f100_actif ? `${nfcp1}%` : \"-\";\n\nlet tag =\n (!HA_ON)\n ? \"🚫 HA OFF\"\n : (horloge === 1\n ? \"⚡ CP1 actif (22–6h)\"\n : \"⏱️ CP1 inactif\");\n\nlet logTxt =\n `${tag}` +\n ` | SoC=${soc}%` +\n ` | Seuil=${seuil}%` +\n ` | Forçage=${f100_txt}` +\n ` | NiveauForcé=${nfcp1_txt}` +\n ` | DuréeHC=${duree_hhmm}` +\n ` | Horloge=${horloge}` +\n ` | havalid=${havalid}` +\n ` | Sortie1=${actif_payload}`;\n \ndebug = { payload: logTxt };\n\n// ===================================================================\n// 5️⃣ Sorties Node-RED\n// ===================================================================\nlet seuilMsg = { payload: seuil };\n\nreturn [\n (actif_payload === 7) ? valid : devalid, // 1. Commande ESS (7 / -1)\n hDebutS, // 2. Heure de début (s)\n duree, // 3. Durée (s)\n debug, // 4. Log texte\n seuilMsg, // 5. Seuil SoC\n autoconsoMsg // 6. Autoconso placeholder\n];\n",
"outputs": 6,
"timeout": "",
"noerr": 0,
"initialize": "// Le code ajouté ici sera exécuté une fois\n// à chaque démarrage du noeud.\nflow.set('cligno', \"off\");",
"finalize": "",
"libs": [],
"x": 490,
"y": 740,
"wires": [
[
"7a72bd69fea8ec17",
"cbe71d4c64597512"
],
[
"c7bade8283007b6b",
"317a0cfb20712290"
],
[
"31630e5509a5a01c",
"4654dd1c8bf3344e"
],
[
"d9d9937809ad2185",
"7b1ac5814db8a9ac"
],
[
"38b5d65f62960b22",
"4047bb58bd817588",
"4b84dbc2655e1bcd"
],
[
"f5bd2bcbcc8eb176"
]
],
"outputLabels": [
"Validation",
"Heure Début",
"Durée",
"Log",
"Ecart SOC",
"H Début"
]
},
{
"id": "38b5d65f62960b22",
"type": "victron-output-ess",
"z": "5fe7408840b1cdf3",
"service": "com.victronenergy.settings",
"path": "/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Soc",
"serviceObj": {
"service": "com.victronenergy.settings",
"name": "Venus settings"
},
"pathObj": {
"path": "/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Soc",
"type": "integer",
"name": "Schedule 1: State of charge (%)",
"writable": true
},
"initial": "",
"name": "Cible SOC ",
"onlyChanges": false,
"x": 730,
"y": 920,
"wires": []
},
{
"id": "cbe71d4c64597512",
"type": "victron-output-ess",
"z": "5fe7408840b1cdf3",
"service": "com.victronenergy.settings",
"path": "/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Day",
"serviceObj": {
"service": "com.victronenergy.settings",
"name": "Venus settings"
},
"pathObj": {
"path": "/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Day",
"type": "enum",
"name": "Schedule 1: Day",
"enum": {
"0": "Sunday",
"1": "Monday",
"2": "Tuesday",
"3": "Wednesday",
"4": "Thursday",
"5": "Friday",
"6": "Saturday",
"7": "Every day",
"8": "Weekdays",
"9": "Weekends",
"11": "Monthly",
"-1": "Disabled"
},
"remarks": "<p>A negative value means that the schedule has been de-activated.</p>",
"mode": "both"
},
"name": "Validation CP",
"onlyChanges": false,
"x": 740,
"y": 540,
"wires": []
},
{
"id": "c7bade8283007b6b",
"type": "victron-output-ess",
"z": "5fe7408840b1cdf3",
"service": "com.victronenergy.settings",
"path": "/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Start",
"serviceObj": {
"service": "com.victronenergy.settings",
"name": "Venus settings"
},
"pathObj": {
"path": "/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Start",
"type": "integer",
"name": "Schedule 1: Start (seconds after midnight)",
"writable": true
},
"initial": "",
"name": "Heure Début",
"onlyChanges": false,
"x": 690,
"y": 680,
"wires": []
},
{
"id": "31630e5509a5a01c",
"type": "victron-output-ess",
"z": "5fe7408840b1cdf3",
"service": "com.victronenergy.settings",
"path": "/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Duration",
"serviceObj": {
"service": "com.victronenergy.settings",
"name": "Venus settings"
},
"pathObj": {
"path": "/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Duration",
"type": "integer",
"name": "Schedule 1: Duration (seconds)",
"writable": true
},
"initial": "",
"name": "Durée",
"onlyChanges": false,
"x": 690,
"y": 740,
"wires": []
},
{
"id": "c550426132409e4e",
"type": "debug",
"z": "5fe7408840b1cdf3",
"name": "Duree",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "true",
"targetType": "full",
"statusVal": "payload",
"statusType": "auto",
"x": 970,
"y": 740,
"wires": []
},
{
"id": "665a65c2ac7c84ce",
"type": "bigtimer",
"z": "5fe7408840b1cdf3",
"outtopic": "",
"outpayload1": "",
"outpayload2": "",
"name": "Horl CP 22->6",
"comment": "",
"lat": "43.91905434993742",
"lon": "2.1979451056884747",
"starttime": "1320",
"endtime": "345",
"starttime2": "0",
"endtime2": "0",
"startoff": 0,
"endoff": 0,
"startoff2": 0,
"endoff2": 0,
"offs": 0,
"outtext1": "\"on\"",
"outtext2": "\"off\"",
"timeout": 1440,
"sun": true,
"mon": true,
"tue": true,
"wed": true,
"thu": true,
"fri": true,
"sat": true,
"jan": true,
"feb": true,
"mar": true,
"apr": true,
"may": true,
"jun": true,
"jul": true,
"aug": true,
"sep": true,
"oct": true,
"nov": true,
"dec": true,
"day1": 0,
"month1": 0,
"day2": 0,
"month2": 0,
"day3": 0,
"month3": 0,
"day4": 0,
"month4": 0,
"day5": 0,
"month5": 0,
"day6": 0,
"month6": 0,
"day7": 0,
"month7": 0,
"day8": 0,
"month8": 0,
"day9": 0,
"month9": 0,
"day10": 0,
"month10": 0,
"day11": 0,
"month11": 0,
"day12": 0,
"month12": 0,
"d1": 0,
"w1": 0,
"d2": 0,
"w2": 0,
"d3": 0,
"w3": 0,
"d4": 0,
"w4": 0,
"d5": 0,
"w5": 0,
"d6": 0,
"w6": 0,
"xday1": 0,
"xmonth1": 0,
"xday2": 0,
"xmonth2": 0,
"xday3": 0,
"xmonth3": 0,
"xday4": 0,
"xmonth4": 0,
"xday5": 0,
"xmonth5": 0,
"xday6": 0,
"xmonth6": 0,
"xday7": 0,
"xmonth7": 0,
"xday8": 0,
"xmonth8": 0,
"xday9": 0,
"xmonth9": 0,
"xday10": 0,
"xmonth10": 0,
"xday11": 0,
"xmonth11": 0,
"xday12": 0,
"xmonth12": 0,
"xd1": 0,
"xw1": 0,
"xd2": 0,
"xw2": 0,
"xd3": 0,
"xw3": 0,
"xd4": 0,
"xw4": 0,
"xd5": 0,
"xw5": 0,
"xd6": 0,
"xw6": 0,
"suspend": false,
"random": false,
"randon1": false,
"randoff1": false,
"randon2": false,
"randoff2": false,
"repeat": true,
"atstart": true,
"odd": false,
"even": false,
"x": 140,
"y": 760,
"wires": [
[],
[
"311d550b1ef83e2d"
],
[]
]
},
{
"id": "84e18d2c39c07038",
"type": "change",
"z": "5fe7408840b1cdf3",
"name": "Sos F100",
"rules": [
{
"t": "set",
"p": "forc100",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 280,
"y": 620,
"wires": [
[
"e23bf7ba06f7abf5"
]
],
"info": "*** mermaid\njourney\n title My working day\n section Go to work\n Make tea: 5: Me\n Go upstairs: 3: Me\n Do work: 1: Me, Cat\n section Go home\n Go downstairs: 5: Me\n Sit down: 5: Me"
},
{
"id": "7c1b7bd28ca87a43",
"type": "change",
"z": "5fe7408840b1cdf3",
"name": "Sos VCP",
"rules": [
{
"t": "set",
"p": "valid_cp_ess",
"pt": "global",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 280,
"y": 480,
"wires": [
[
"e23bf7ba06f7abf5"
]
],
"info": "*** mermaid\njourney\n title My working day\n section Go to work\n Make tea: 5: Me\n Go upstairs: 3: Me\n Do work: 1: Me, Cat\n section Go home\n Go downstairs: 5: Me\n Sit down: 5: Me"
},
{
"id": "e63917c57e76284a",
"type": "change",
"z": "5fe7408840b1cdf3",
"name": "Sos NF CP1",
"rules": [
{
"t": "set",
"p": "niveauforcp1",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 270,
"y": 540,
"wires": [
[
"e23bf7ba06f7abf5"
]
],
"info": "*** mermaid\njourney\n title My working day\n section Go to work\n Make tea: 5: Me\n Go upstairs: 3: Me\n Do work: 1: Me, Cat\n section Go home\n Go downstairs: 5: Me\n Sit down: 5: Me"
},
{
"id": "5eacf6a66623ab28",
"type": "mqtt out",
"z": "5fe7408840b1cdf3",
"name": "Valid CP",
"topic": "mp2/multiplus2/valide_cp",
"qos": "",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "502248144035edbf",
"x": 900,
"y": 580,
"wires": []
},
{
"id": "5ddb73a1d5111255",
"type": "mqtt out",
"z": "5fe7408840b1cdf3",
"name": "H DEBUT",
"topic": "mp2/dess_remy/h_debut",
"qos": "",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "502248144035edbf",
"x": 980,
"y": 680,
"wires": []
},
{
"id": "d9d9937809ad2185",
"type": "debug",
"z": "5fe7408840b1cdf3",
"name": "debug 62",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "true",
"targetType": "full",
"statusVal": "payload",
"statusType": "auto",
"x": 900,
"y": 860,
"wires": []
},
{
"id": "646d8070d09a18b1",
"type": "inject",
"z": "5fe7408840b1cdf3",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 270,
"y": 700,
"wires": [
[
"e23bf7ba06f7abf5"
]
]
},
{
"id": "7a72bd69fea8ec17",
"type": "function",
"z": "5fe7408840b1cdf3",
"name": "Convert",
"func": "var code = msg.payload;\n\n// Tableau de correspondance code → texte\nvar mapper = {\n \"-1\": \"off\",\n \"7\": \"on\"\n};\n\n// Fonction pour récupérer le texte correspondant\nfunction extraireTexte(valeur) {\n return mapper[valeur.toString()] || \"Etat Inconnu\";\n}\n\n// Appliquer le mapping\nmsg.payload = extraireTexte(code);\n\nreturn msg;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 700,
"y": 620,
"wires": [
[
"5eacf6a66623ab28",
"04096105ab259966"
]
]
},
{
"id": "658208f94208bd9f",
"type": "ui_chart",
"z": "5fe7408840b1cdf3",
"name": "",
"group": "c8a7d12bb7f76f27",
"order": 9,
"width": 0,
"height": 0,
"label": "SOC",
"chartType": "line",
"legend": "false",
"xformat": "HH:mm:ss",
"interpolate": "linear",
"nodata": "",
"dot": false,
"ymin": "0",
"ymax": "100",
"removeOlder": "2",
"removeOlderPoints": "",
"removeOlderUnit": "3600",
"cutout": 0,
"useOneColor": false,
"useUTC": true,
"colors": [
"#1f77b4",
"#aec7e8",
"#ff7f0e",
"#2ca02c",
"#98df8a",
"#d62728",
"#ff9896",
"#9467bd",
"#c5b0d5"
],
"outputs": 1,
"useDifferentColor": false,
"className": "",
"x": 650,
"y": 1080,
"wires": [
[]
]
},
{
"id": "f5bd2bcbcc8eb176",
"type": "victron-output-ess",
"z": "5fe7408840b1cdf3",
"service": "com.victronenergy.settings",
"path": "/Settings/CGwacs/BatteryLife/Schedule/Charge/0/AllowDischarge",
"serviceObj": {
"service": "com.victronenergy.settings",
"name": "Venus settings"
},
"pathObj": {
"path": "/Settings/CGwacs/BatteryLife/Schedule/Charge/0/AllowDischarge",
"type": "enum",
"name": "Schedule 1: Self-consumption above limit",
"enum": {
"0": "Yes",
"1": "No"
},
"mode": "both"
},
"name": "Autoconsommation",
"onlyChanges": false,
"x": 730,
"y": 1000,
"wires": []
},
{
"id": "7b1ac5814db8a9ac",
"type": "ui_text",
"z": "5fe7408840b1cdf3",
"group": "c8a7d12bb7f76f27",
"order": 1,
"width": "8",
"height": "2",
"name": "",
"label": "Log:",
"format": "{{msg.payload}}",
"layout": "row-spread",
"className": "",
"style": false,
"font": "",
"fontSize": 16,
"color": "#000000",
"x": 730,
"y": 840,
"wires": []
},
{
"id": "65f2ed67762e936f",
"type": "link out",
"z": "5fe7408840b1cdf3",
"name": "Out_valid_cp",
"mode": "link",
"links": [
"419dd3d3081142a4"
],
"x": 585,
"y": 480,
"wires": []
},
{
"id": "9a02956e8c37d773",
"type": "ui_gauge",
"z": "5fe7408840b1cdf3",
"name": "",
"group": "c8a7d12bb7f76f27",
"order": 8,
"width": "3",
"height": "3",
"gtype": "gage",
"title": "Previ Prod J",
"label": "kWh",
"format": "{{value}}",
"min": 0,
"max": "30",
"colors": [
"#00b500",
"#e6e600",
"#ca3838"
],
"seg1": "",
"seg2": "",
"diff": false,
"className": "",
"x": 770,
"y": 1140,
"wires": []
},
{
"id": "6901655995625596",
"type": "ui_gauge",
"z": "5fe7408840b1cdf3",
"name": "",
"group": "c8a7d12bb7f76f27",
"order": 2,
"width": "3",
"height": "3",
"gtype": "gage",
"title": "SOC",
"label": "%",
"format": "{{value}}",
"min": 0,
"max": "100",
"colors": [
"#00b500",
"#e6e600",
"#ca3838"
],
"seg1": "",
"seg2": "",
"diff": false,
"className": "",
"x": 810,
"y": 1060,
"wires": []
},
{
"id": "f7ddcf1e407745e9",
"type": "victron-input-battery",
"z": "5fe7408840b1cdf3",
"service": "com.victronenergy.battery/277",
"path": "/Soc",
"serviceObj": {
"service": "com.victronenergy.battery/277",
"name": "SmartShunt 500A/50mV"
},
"pathObj": {
"path": "/Soc",
"type": "float",
"name": "State of charge (%)"
},
"initial": "",
"name": "SOC Batteries",
"onlyChanges": false,
"roundValues": "2",
"x": 130,
"y": 1100,
"wires": [
[
"0af06d70cb632e52"
]
]
},
{
"id": "4b84dbc2655e1bcd",
"type": "ui_gauge",
"z": "5fe7408840b1cdf3",
"name": "",
"group": "c8a7d12bb7f76f27",
"order": 3,
"width": "3",
"height": "3",
"gtype": "gage",
"title": "SOC Cible",
"label": "%",
"format": "{{value}}",
"min": 0,
"max": "100",
"colors": [
"#00b500",
"#e6e600",
"#ca3838"
],
"seg1": "",
"seg2": "",
"diff": false,
"className": "",
"x": 910,
"y": 920,
"wires": []
},
{
"id": "926f31e0a94a4933",
"type": "vrm-api",
"z": "5fe7408840b1cdf3",
"vrm": "5e3a89e5eaeb1cb4",
"name": "Prod PV inverter J+1",
"api_type": "installations",
"idUser": "",
"idSite": "223181",
"installations": "stats",
"attribute": "vrm_pv_inverter_yield_fc",
"stats_interval": "days",
"show_instance": false,
"stats_start": "bot",
"stats_end": "86400",
"use_utc": false,
"gps_start": "",
"gps_end": "",
"instance": "",
"vrm_id": "",
"b_max": "",
"tb_max": "",
"fb_max": "",
"tg_max": "",
"fg_max": "",
"b_cycle_cost": "",
"buy_price_formula": "",
"sell_price_formula": "",
"green_mode_on": true,
"feed_in_possible": true,
"feed_in_control_on": true,
"b_goal_hour": "",
"b_goal_SOC": "",
"store_in_global_context": false,
"verbose": false,
"x": 440,
"y": 180,
"wires": [
[
"09d902bf614f3f7a"
]
]
},
{
"id": "6027c79e01cbefde",
"type": "inject",
"z": "5fe7408840b1cdf3",
"name": "Ttes les H de 22 à Minuit",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "0 22-23 * * *",
"once": false,
"onceDelay": "30",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 150,
"y": 320,
"wires": [
[
"473056f3c0feadfa",
"926f31e0a94a4933"
]
]
},
{
"id": "bce3f68e52fd2d36",
"type": "mqtt out",
"z": "5fe7408840b1cdf3",
"name": "Previ. prod. MO",
"topic": "mp2/dess_remy/prod_onduleur",
"qos": "",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "502248144035edbf",
"x": 980,
"y": 160,
"wires": []
},
{
"id": "473056f3c0feadfa",
"type": "vrm-api",
"z": "5fe7408840b1cdf3",
"vrm": "5e3a89e5eaeb1cb4",
"name": "Prod MPPT1 J+1",
"api_type": "installations",
"idUser": "",
"idSite": "223181",
"installations": "stats",
"attribute": "vrm_pv_charger_yield_fc",
"stats_interval": "2hours",
"show_instance": false,
"stats_start": "bot",
"stats_end": "86400",
"use_utc": false,
"gps_start": "",
"gps_end": "",
"instance": "",
"vrm_id": "",
"b_max": "",
"tb_max": "",
"fb_max": "",
"tg_max": "",
"fg_max": "",
"b_cycle_cost": "",
"buy_price_formula": "",
"sell_price_formula": "",
"green_mode_on": true,
"feed_in_possible": true,
"feed_in_control_on": true,
"b_goal_hour": "",
"b_goal_SOC": "",
"store_in_global_context": false,
"verbose": false,
"x": 430,
"y": 320,
"wires": [
[
"30376bba6a1407b1"
]
]
},
{
"id": "0a9bb8cea9b26961",
"type": "mqtt out",
"z": "5fe7408840b1cdf3",
"name": "Predi. prod. MPPT1",
"topic": "mp2/dess_remy/prod_mppt1",
"qos": "",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "502248144035edbf",
"x": 970,
"y": 320,
"wires": []
},
{
"id": "baed2acaef234192",
"type": "mqtt out",
"z": "5fe7408840b1cdf3",
"name": "Prediction prod total",
"topic": "mp2/dess_remy/prod_total",
"qos": "2",
"retain": "true",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "502248144035edbf",
"x": 980,
"y": 240,
"wires": []
},
{
"id": "04f96335d4b31ec0",
"type": "link out",
"z": "5fe7408840b1cdf3",
"name": "out_prod_total",
"mode": "link",
"links": [
"5010cfdb6709a7ec"
],
"x": 885,
"y": 200,
"wires": []
},
{
"id": "80bbf22798b0087c",
"type": "comment",
"z": "5fe7408840b1cdf3",
"name": "Estimation de la production PV",
"info": "",
"x": 570,
"y": 40,
"wires": []
},
{
"id": "733a551f56144169",
"type": "function",
"z": "5fe7408840b1cdf3",
"name": "Somme des deux",
"func": "// ===================================================================\n// Additionne les prévisions : previmo + previmppt1\n// ===================================================================\n\n// Lecture sécurisée des variables Flow\nlet p = Number(flow.get('previmo')) || 0;\nlet c = Number(flow.get('previmppt1')) || 0;\n\n// Calcul de la somme\nlet sum = p + c;\n\n// Mise à jour du statut visuel du nœud\nnode.status({\n fill: \"green\",\n shape: \"dot\",\n text: `previmo=${p} + previmppt1=${c} → total=${sum}`\n});\n\n// Sortie du résultat dans msg.payload\nmsg.payload = sum;\nreturn msg;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 730,
"y": 240,
"wires": [
[
"baed2acaef234192",
"04f96335d4b31ec0"
]
]
},
{
"id": "09d902bf614f3f7a",
"type": "function",
"z": "5fe7408840b1cdf3",
"name": "set previmo",
"func": "// ===================================================================\n// Extrait la prévision PV (Wh) -> renvoie kWh arrondi (entier)\n// lit data.totals.vrm_pv_inverter_yield_fc\n// ===================================================================\n\nconst d = msg.payload;\nlet found = false;\nlet totalWh = 0;\n\n// Validation et extraction\nif (d && d.totals && d.totals.vrm_pv_inverter_yield_fc != null) {\n const v = Number(d.totals.vrm_pv_inverter_yield_fc);\n if (Number.isFinite(v)) {\n totalWh = v;\n found = true;\n }\n}\n\n// Conversion en kWh arrondi à l'entier\nlet totalKWh = Math.round(totalWh / 1000);\n\n// Sortie + mémorisation + statut\nif (found) {\n msg.payload = totalKWh;\n flow.set(\"previmo\", totalKWh);\n node.status({ fill: \"green\", shape: \"dot\", text: `Previ=${totalKWh} kWh` });\n} else {\n msg.payload = 0;\n flow.set(\"previmo\", 0);\n node.warn(\"Total de production PV non trouvé ou invalide dans le JSON !\");\n node.status({ fill: \"red\", shape: \"dot\", text: \"Previ indisponible\" });\n}\n\nreturn msg;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 710,
"y": 160,
"wires": [
[
"bce3f68e52fd2d36",
"733a551f56144169"
]
]
},
{
"id": "30376bba6a1407b1",
"type": "function",
"z": "5fe7408840b1cdf3",
"name": "set previmppt1",
"func": "// ===================================================================\n// Extrait la prévision PV du MPPT1 (Wh) → kWh arrondi\n// lit data.totals.vrm_pv_charger_yield_fc\n// ===================================================================\n\nconst d = msg.payload;\nlet found = false;\nlet totalWh = 0;\n\n// Validation et extraction\nif (d && d.totals && d.totals.vrm_pv_charger_yield_fc != null) {\n const v = Number(d.totals.vrm_pv_charger_yield_fc);\n if (Number.isFinite(v)) {\n totalWh = v;\n found = true;\n }\n}\n\n// Conversion en kWh arrondi à l'entier\nlet totalKWh = Math.round(totalWh / 1000);\n\n// Sortie + mémorisation + statut\nif (found) {\n msg.payload = totalKWh;\n flow.set(\"previmppt1\", totalKWh);\n node.status({ fill: \"green\", shape: \"dot\", text: `PreviMPPT1=${totalKWh} kWh` });\n} else {\n msg.payload = 0;\n flow.set(\"previmppt1\", 0);\n node.warn(\"Total de production PV (MPPT1) non trouvé ou invalide !\");\n node.status({ fill: \"red\", shape: \"dot\", text: \"PreviMPPT1 indisponible\" });\n}\n\nreturn msg;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 720,
"y": 320,
"wires": [
[
"0a9bb8cea9b26961",
"733a551f56144169"
]
]
},
{
"id": "a8b2fec6fdd2265b",
"type": "mqtt in",
"z": "5fe7408840b1cdf3",
"name": "Forcage CP 100%",
"topic": "ha/mp2/cp/forcage100",
"qos": "2",
"datatype": "auto-detect",
"broker": "502248144035edbf",
"nl": false,
"rap": true,
"rh": 0,
"inputs": 0,
"x": 90,
"y": 620,
"wires": [
[
"84e18d2c39c07038",
"733ab2eac5f9cbac"
]
]
},
{
"id": "ce7397f52af03a02",
"type": "mqtt in",
"z": "5fe7408840b1cdf3",
"name": "Valid ESS MP1",
"topic": "ha/mp2/cp/validcp",
"qos": "2",
"datatype": "auto-detect",
"broker": "502248144035edbf",
"nl": false,
"rap": true,
"rh": 0,
"inputs": 0,
"x": 80,
"y": 480,
"wires": [
[
"be13222435c38a50",
"7c1b7bd28ca87a43",
"65f2ed67762e936f"
]
]
},
{
"id": "8ef38bc25af154c7",
"type": "mqtt in",
"z": "5fe7408840b1cdf3",
"name": "Niveau Forcage CP1",
"topic": "ha/mp2/cp/niveauforcagecp1",
"qos": "2",
"datatype": "auto-detect",
"broker": "502248144035edbf",
"nl": false,
"rap": true,
"rh": 0,
"inputs": 0,
"x": 90,
"y": 540,
"wires": [
[
"e63917c57e76284a",
"6c53e12e5fa4b06e"
]
]
},
{
"id": "6c53e12e5fa4b06e",
"type": "debug",
"z": "5fe7408840b1cdf3",
"name": "debug 69",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "payload",
"targetType": "msg",
"statusVal": "payload",
"statusType": "auto",
"x": 480,
"y": 540,
"wires": []
},
{
"id": "733ab2eac5f9cbac",
"type": "debug",
"z": "5fe7408840b1cdf3",
"name": "debug 70",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "payload",
"targetType": "msg",
"statusVal": "payload",
"statusType": "auto",
"x": 480,
"y": 620,
"wires": []
},
{
"id": "be13222435c38a50",
"type": "debug",
"z": "5fe7408840b1cdf3",
"name": "debug 71",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "payload",
"targetType": "msg",
"statusVal": "payload",
"statusType": "auto",
"x": 460,
"y": 480,
"wires": []
},
{
"id": "04096105ab259966",
"type": "debug",
"z": "5fe7408840b1cdf3",
"name": "Status",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "payload",
"targetType": "msg",
"statusVal": "payload",
"statusType": "auto",
"x": 930,
"y": 520,
"wires": []
},
{
"id": "afefc88f251e2967",
"type": "vrm-api",
"z": "5fe7408840b1cdf3",
"vrm": "5e3a89e5eaeb1cb4",
"name": "Prod PV inverter J",
"api_type": "installations",
"idUser": "",
"idSite": "223181",
"installations": "stats",
"attribute": "vrm_pv_inverter_yield_fc",
"stats_interval": "2hours",
"show_instance": false,
"stats_start": "bod",
"stats_end": "eod",
"use_utc": false,
"gps_start": "",
"gps_end": "",
"instance": "",
"vrm_id": "",
"b_max": "",
"tb_max": "",
"fb_max": "",
"tg_max": "",
"fg_max": "",
"b_cycle_cost": "",
"buy_price_formula": "",
"sell_price_formula": "",
"green_mode_on": true,
"feed_in_possible": true,
"feed_in_control_on": true,
"b_goal_hour": "",
"b_goal_SOC": "",
"store_in_global_context": false,
"verbose": false,
"x": 430,
"y": 120,
"wires": [
[
"09d902bf614f3f7a"
]
]
},
{
"id": "000a24d128ee7177",
"type": "inject",
"z": "5fe7408840b1cdf3",
"name": "Ttes Les H de Minuit à 6:00",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "0 0-5 * * *",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 160,
"y": 240,
"wires": [
[
"afefc88f251e2967",
"c291619a382d1694"
]
]
},
{
"id": "c291619a382d1694",
"type": "vrm-api",
"z": "5fe7408840b1cdf3",
"vrm": "5e3a89e5eaeb1cb4",
"name": "Prod MPPT1 J",
"api_type": "installations",
"idUser": "",
"idSite": "223181",
"installations": "stats",
"attribute": "vrm_pv_charger_yield_fc",
"stats_interval": "days",
"show_instance": false,
"stats_start": "bod",
"stats_end": "eod",
"use_utc": false,
"gps_start": "",
"gps_end": "",
"instance": "",
"vrm_id": "",
"b_max": "",
"tb_max": "",
"fb_max": "",
"tg_max": "",
"fg_max": "",
"b_cycle_cost": "",
"buy_price_formula": "",
"sell_price_formula": "",
"green_mode_on": true,
"feed_in_possible": true,
"feed_in_control_on": true,
"b_goal_hour": "",
"b_goal_SOC": "",
"store_in_global_context": false,
"verbose": false,
"x": 420,
"y": 260,
"wires": [
[
"30376bba6a1407b1"
]
]
},
{
"id": "83b23b5304a18717",
"type": "mqtt out",
"z": "5fe7408840b1cdf3",
"name": "Durée",
"topic": "mp2/dess_remy/duree",
"qos": "",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "502248144035edbf",
"x": 970,
"y": 800,
"wires": []
},
{
"id": "4654dd1c8bf3344e",
"type": "function",
"z": "5fe7408840b1cdf3",
"name": "Convert",
"func": "// Entrée attendue : msg.payload = durée en secondes (number ou string)\n// Sortie : msg.payload = \"HH:MM\" (string)\n\nlet tps = Number(msg.payload);\n\n// Garde-fous\nif (!isFinite(tps) || tps < 0) {\n // Choix 1 : ne rien envoyer si invalide\n return null;\n\n // Choix 2 (alternative) : envoyer \"00:00\"\n // tps = 0;\n}\n\n// Arrondi propre (si jamais tu as des floats)\ntps = Math.round(tps);\n\nconst h = Math.floor(tps / 3600);\nconst m = Math.floor((tps % 3600) / 60);\n\nmsg.payload = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;\n\n// Optionnel : pour tracer\n// msg.topic = \"ton/topic/mqtt/duree_hhmm\";\n\nreturn msg;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 800,
"y": 780,
"wires": [
[
"c550426132409e4e",
"83b23b5304a18717"
]
]
},
{
"id": "351fc1fc4d40c24f",
"type": "function",
"z": "5fe7408840b1cdf3",
"name": "H Dynamique",
"func": "// --- Paramètres ---\nconst CAP_AH = 300; // capacité totale\nconst I_AVG = 40; // courant moyen utile (A)\nconst ABS_MIN = 60; // marge absorption/synchro (min)\nconst MIN_WIN = 90; // fenêtre minimale (min)\nconst MAX_WIN = 8 * 60; // garde-fou\n\n// Fenêtre imposée (traverse minuit)\nconst START_FLOOR_MIN = 22 * 60 + 0; // 22:00\nconst END_FIXED_MIN = 5 * 60 + 40; // 05:40\n\n// Entrée SoC (depuis flow)\nlet soc = Number(flow.get('soc'));\nif (!isFinite(soc)) soc = 0;\nsoc = Math.max(0, Math.min(100, soc));\n\n// Objectif\nlet target = Number(flow.get('seuil_soc'));\nif (!isFinite(target)) target = 100;\ntarget = Math.max(0, Math.min(100, target));\n\n// --- Calcul besoin ---\nlet delta = Math.max(0, target - soc);\nlet ah_needed = (delta / 100) * CAP_AH;\n\n// Temps bulk estimé\nlet bulk_min = (I_AVG > 0) ? (ah_needed / I_AVG) * 60 : 0;\n\n// Temps absorption (si on vise haut)\nlet abs_min = (target >= 95) ? ABS_MIN : 0;\n\nlet need_min = Math.ceil(bulk_min + abs_min);\nneed_min = Math.max(MIN_WIN, Math.min(MAX_WIN, need_min));\n\n// --- Calcul début théorique à partir de la fin ---\nlet start_min_raw = END_FIXED_MIN - need_min;\nlet start_min = start_min_raw;\n\nconst STEP = 5; // minutes\nstart_min = Math.floor(start_min / STEP) * STEP;\n\nlet wrapped = false;\nif (start_min < 0) { start_min += 24 * 60; wrapped = true; } // passage minuit\n\n// --- Appliquer le plancher 22:00 EN GÉRANT MINUIT ---\n// Fenêtre autorisée : [22:00..23:59] U [00:00..05:40]\nconst inAllowedWindow = (start_min >= START_FLOOR_MIN) || (start_min <= END_FIXED_MIN);\nconst clampedTo2200 = (!inAllowedWindow);\nif (clampedTo2200) start_min = START_FLOOR_MIN;\n\n// --- Fonction format HH:MM ---\nfunction hhmm(mins) {\n const h = Math.floor(mins / 60);\n const m = mins % 60;\n return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}`;\n}\n\nconst start_hhmm = hhmm(start_min);\nconst end_hhmm = hhmm(END_FIXED_MIN);\n\n// --- Vérifier si la fenêtre est suffisante ---\nconst available_min = (END_FIXED_MIN >= START_FLOOR_MIN)\n ? (END_FIXED_MIN - START_FLOOR_MIN)\n : (24 * 60 - START_FLOOR_MIN + END_FIXED_MIN);\n\nconst ok = (need_min <= available_min);\n\n// Status + debug\nnode.status({\n text: ok\n ? `BT ${start_hhmm}→${end_hhmm} (SoC=${soc}%, cible=${target}%, besoin=${need_min}m)`\n : `ALERTE: besoin=${need_min}m > dispo=${available_min}m (SoC=${soc}%, cible=${target}%)`\n});\n\n// Expose pour debug/HA si tu veux\nflow.set('bt_start', start_hhmm);\nflow.set('bt_end', end_hhmm);\nflow.set('bt_need_min', need_min);\nflow.set('bt_available_min', available_min);\nflow.set('bt_ok', ok);\n\n// --- Messages BigTimer ---\nconst msgOn = { payload: `on_override ${start_hhmm}` };\nconst msgOff = { payload: `off_override ${end_hhmm}` };\nconst msgSync = { payload: \"sync\" };\n\n// --- ✅ Sortie 4 : log pertinent ---\nconst now = new Date();\nconst now_hhmm = hhmm(now.getHours() * 60 + now.getMinutes());\n\nconst start_raw_hhmm = hhmm(((start_min_raw % (24*60)) + (24*60)) % (24*60));\n\nconst infoFlags = [\n wrapped ? \"wrap\" : null,\n clampedTo2200 ? \"clamp22\" : null\n].filter(Boolean).join(\",\");\n\nconst logTxt =\n `BT calc @ ${now_hhmm}` +\n ` | SoC=${soc.toFixed(1)}% → cible=${target.toFixed(0)}% (écart=${delta.toFixed(1)}%)` +\n ` | Énergie à charger=${ah_needed.toFixed(1)} Ah` +\n ` | Estimation: bulk=${Math.ceil(bulk_min)} min + absorption=${abs_min} min → besoin=${need_min} min` +\n ` | Début théorique=${start_raw_hhmm} → Début retenu=${start_hhmm}` +\n ` | Fin=${end_hhmm}` +\n ` | Fenêtre dispo=${available_min} min` +\n ` | OK=${ok}` +\n (infoFlags ? ` | Ajustements=${infoFlags}` : \"\");\n\n\nconst msgLog = { payload: logTxt };\n\nreturn [msgOn, msgOff, msgSync, msgLog];\n",
"outputs": 4,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 120,
"y": 860,
"wires": [
[
"665a65c2ac7c84ce"
],
[
"665a65c2ac7c84ce"
],
[
"6fc4810911c263cc"
],
[
"701744df73f35cf2"
]
]
},
{
"id": "f1509d8a0752eb41",
"type": "inject",
"z": "5fe7408840b1cdf3",
"name": "Declenche Calcul",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "45 21 * * *",
"once": true,
"onceDelay": "5",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 130,
"y": 980,
"wires": [
[
"351fc1fc4d40c24f"
]
]
},
{
"id": "317a0cfb20712290",
"type": "function",
"z": "5fe7408840b1cdf3",
"name": "Convert",
"func": "// Entrée attendue : msg.payload = durée en secondes (number ou string)\n// Sortie : msg.payload = \"HH:MM\" (string)\n\nlet tps = Number(msg.payload);\n\n// Garde-fous\nif (!isFinite(tps) || tps < 0) {\n // Choix 1 : ne rien envoyer si invalide\n return null;\n\n // Choix 2 (alternative) : envoyer \"00:00\"\n // tps = 0;\n}\n\n// Arrondi propre (si jamais tu as des floats)\ntps = Math.round(tps);\n\nconst h = Math.floor(tps / 3600);\nconst m = Math.floor((tps % 3600) / 60);\n\nmsg.payload = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;\n\n// Optionnel : pour tracer\n// msg.topic = \"ton/topic/mqtt/duree_hhmm\";\n\nreturn msg;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 840,
"y": 680,
"wires": [
[
"5ddb73a1d5111255",
"f7fa6e68ca3c9dd5"
]
]
},
{
"id": "f7fa6e68ca3c9dd5",
"type": "debug",
"z": "5fe7408840b1cdf3",
"name": "Duree",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "true",
"targetType": "full",
"statusVal": "payload",
"statusType": "auto",
"x": 970,
"y": 620,
"wires": []
},
{
"id": "6fc4810911c263cc",
"type": "delay",
"z": "5fe7408840b1cdf3",
"name": "",
"pauseType": "delay",
"timeout": "200",
"timeoutUnits": "milliseconds",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": false,
"outputs": 1,
"x": 330,
"y": 860,
"wires": [
[
"665a65c2ac7c84ce"
]
]
},
{
"id": "67bc6865c7406fee",
"type": "function",
"z": "5fe7408840b1cdf3",
"name": "Declenche sur SOC",
"func": "// Déclenchement SoC-driven avec rate limit + fenêtre horaire\n// + Ne recalculer QUE si BigTimer est OFF (horloge != 1)\n\nconst MIN_DELTA_SOC = 0.5; // % mini pour recalcul\nconst MIN_PERIOD_S = 10 * 60; // 10 min mini entre recalculs\n\nconst NOW = new Date();\nconst nowMin = NOW.getHours() * 60 + NOW.getMinutes();\n\n// Fenêtre où recalculer (ex: 21:30 -> 05:30)\nconst WIN_START = 22 * 60 + 0;\nconst WIN_END = 5 * 60 + 30;\nconst inWindow = (nowMin >= WIN_START) || (nowMin <= WIN_END);\nif (!inWindow) return null;\n\n// ✅ STOP si BigTimer est déjà ON\nlet horloge = Number(flow.get('horloge')); // 1 si BigTimer ON\nif (horloge === 1) return null;\n\n// SoC courant\nlet soc = flow.get('soc'); \nif (!isFinite(soc)) return null;\n\n// Delta SoC\nconst lastSoc = Number(flow.get('bt_last_soc'));\nif (isFinite(lastSoc) && Math.abs(soc - lastSoc) < MIN_DELTA_SOC) return null;\n\n// Rate limit\nconst lastTs = Number(flow.get('bt_last_ts'));\nconst nowTs = Math.floor(Date.now() / 1000);\nif (isFinite(lastTs) && (nowTs - lastTs) < MIN_PERIOD_S) return null;\n\n// OK -> mémorise\nflow.set('bt_last_soc', soc);\nflow.set('bt_last_ts', nowTs);\n\n// Met à dispo pour ta fonction de calcul\nflow.set('soc', soc);\n\n// Continue vers la fonction \"CALC_HORAIRES_BIGTIMER\"\nreturn msg;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 320,
"y": 1020,
"wires": [
[
"351fc1fc4d40c24f"
]
]
},
{
"id": "701744df73f35cf2",
"type": "ui_text",
"z": "5fe7408840b1cdf3",
"group": "c8a7d12bb7f76f27",
"order": 1,
"width": "8",
"height": "2",
"name": "",
"label": "Log:",
"format": "{{msg.payload}}",
"layout": "row-spread",
"className": "",
"style": false,
"font": "",
"fontSize": 16,
"color": "#000000",
"x": 330,
"y": 920,
"wires": []
},
{
"id": "502248144035edbf",
"type": "mqtt-broker",
"name": "MQTTHA",
"broker": "192.168.0.37",
"port": "1883",
"clientid": "",
"autoConnect": true,
"usetls": false,
"protocolVersion": "4",
"keepalive": "60",
"cleansession": true,
"autoUnsubscribe": true,
"birthTopic": "",
"birthQos": "0",
"birthPayload": "",
"birthMsg": {},
"closeTopic": "",
"closeQos": "0",
"closePayload": "",
"closeMsg": {},
"willTopic": "",
"willQos": "0",
"willPayload": "",
"willMsg": {},
"userProps": "",
"sessionExpiry": ""
},
{
"id": "c8a7d12bb7f76f27",
"type": "ui_group",
"name": "Calcul_SOC",
"tab": "8c1c69b7bf39eb5b",
"order": 1,
"disp": true,
"width": "9",
"collapse": false,
"className": ""
},
{
"id": "5e3a89e5eaeb1cb4",
"type": "config-vrm-api",
"name": "vrm_remy"
},
{
"id": "8c1c69b7bf39eb5b",
"type": "ui_tab",
"name": "MP2",
"icon": "dashboard",
"disabled": false,
"hidden": false
}
]
Mini Table des topics utilisé dans mqtt
- HA → Node-RED
ha/mp2/cp/validcpha/mp2/cp/forcage100ha/mp2/cp/niveauforcagecp1
- Node-RED → HA
mp2/dess_remy/prod_onduleurmp2/dess_remy/prod_mppt1mp2/dess_remy/prod_totalmp2/dess_remy/cible_socmp2/multiplus2/valide_cpmp2/dess_remy/h_debutmp2/dess_remy/duree
