Photovoltaïque Victron : pilotage intelligent du SoC et de la charge programmée avec Node-RED

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 :

  1. 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
  2. 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)
  3. 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.previmo et flow.previmppt1
  • Une fonction Somme des deux additionne les deux prévisions et publie :
    • mp2/dess_remy/prod_onduleur
    • mp2/dess_remy/prod_mppt1
    • mp2/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 = 1 quand on est dans la fenêtre
  • horloge = 0 quand 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 :

  1. 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
  1. Activation CP1
  • si HA autorise ET horloge=1 → activation CP1 (payload=7 sur “Day=Everyday”)
  • sinon → désactivation (payload=-1)
  1. Compatibilité “Start / Duration”
  • conversion minutes → secondes, pour alimenter les settings Victron :
    • /Schedule/Charge/0/Start
    • /Schedule/Charge/0/Duration
  1. 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_soc
  • mp2/multiplus2/valide_cp (converti en on/off via 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 sensor MQTT pour prod_total, cible_soc, duree, etc.
  • utiliser input_boolean / input_number pour validcp, forcage100, niveauforcagecp1, et publier vers les topics ha/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-api utilisé 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/validcp
    • ha/mp2/cp/forcage100
    • ha/mp2/cp/niveauforcagecp1
  • Node-RED → HA
    • mp2/dess_remy/prod_onduleur
    • mp2/dess_remy/prod_mppt1
    • mp2/dess_remy/prod_total
    • mp2/dess_remy/cible_soc
    • mp2/multiplus2/valide_cp
    • mp2/dess_remy/h_debut
    • mp2/dess_remy/duree

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *