{"id":4210,"date":"2026-01-12T16:51:35","date_gmt":"2026-01-12T15:51:35","guid":{"rendered":"https:\/\/domo.rem81.com\/?p=4210"},"modified":"2026-02-16T16:29:21","modified_gmt":"2026-02-16T15:29:21","slug":"photovoltaique-victron-pilotage-intelligent-du-soc-et-de-la-charge-programmee-avec-node-red","status":"publish","type":"post","link":"https:\/\/domo.rem81.com\/index.php\/2026\/01\/12\/photovoltaique-victron-pilotage-intelligent-du-soc-et-de-la-charge-programmee-avec-node-red\/","title":{"rendered":"Photovolta\u00efque Victron : pilotage intelligent du SoC et de la charge programm\u00e9e avec Node-RED"},"content":{"rendered":"\n\n\n\n<h1 class=\"wp-block-heading\">Intro<\/h1>\n\n\n\n<p>Depuis plusieurs mois, mon installation photovolta\u00efque Victron a nettement \u00e9volu\u00e9 : nouveaux capteurs, nouveaux besoins (dont la gestion des jours <strong>EDF Tempo<\/strong>), consommation nocturne, s\u00e9curisation du SoC, et surtout une logique de pilotage plus structur\u00e9e. Plut\u00f4t que de maintenir une s\u00e9rie d\u2019articles \u00e9parpill\u00e9s autour de Node-RED et de quelques flows historiques, j\u2019ai choisi de repartir sur une base saine : <strong>un article unique, complet, et r\u00e9guli\u00e8rement maintenu<\/strong>.<\/p>\n\n\n\n<p>L\u2019objectif de ce billet est simple : <strong>pr\u00e9senter mon architecture de gestion \u00e9nerg\u00e9tique Victron<\/strong>, centr\u00e9e sur :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Node-RED<\/strong> : collecte (VRM), calculs, d\u00e9cisions, orchestration,<\/li>\n\n\n\n<li><strong>MQTT \/ Home Assistant<\/strong> : supervision, autorisations, for\u00e7ages et visibilit\u00e9.<\/li>\n<\/ul>\n\n\n\n<p>Le focus principal porte sur un point cl\u00e9 : <strong>d\u00e9terminer automatiquement une cible de SoC<\/strong> et <strong>piloter intelligemment la charge programm\u00e9e<\/strong> sur une fen\u00eatre nocturne (typique <strong>22h \u2192 6h<\/strong>), en tenant compte des contraintes et opportunit\u00e9s li\u00e9es \u00e0 <strong>Tempo<\/strong> (heures creuses\/heures pleines et jours rouges). Je d\u00e9taille le <em>pourquoi<\/em> et le <em>comment<\/em>, et je fournis les \u00e9l\u00e9ments concrets pour reproduire l\u2019approche : structure du flow, variables manipul\u00e9es, topics MQTT, chemins Venus OS, et points de vigilance.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Objectifs du flow<\/h2>\n\n\n\n<p>Ce flow, nomm\u00e9 <strong>\u00ab DESS R\u00e9my \u00bb<\/strong>, a 3 missions :<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Estimer les pr\u00e9visions J+1 (06:00 \u2192 22:00)<\/strong><\/li>\n<\/ol>\n\n\n\n<ul class=\"wp-block-list\">\n<li>r\u00e9cup\u00e9ration via VRM : pr\u00e9vision <strong>production PV<\/strong> (AC-coupled + MPPT) et pr\u00e9vision <strong>consommation<\/strong><\/li>\n\n\n\n<li>filtrage <strong>J+1<\/strong> et <strong>plage 06:00\u201322:00<\/strong><\/li>\n\n\n\n<li>conversion <strong>Wh \u2192 kWh<\/strong> (arrondi)<\/li>\n\n\n\n<li>stockage en contexte Node-RED + publication MQTT (valeurs \u201cobservables\u201d)<\/li>\n<\/ul>\n\n\n\n<ol start=\"2\" class=\"wp-block-list\">\n<li><strong>Calculer un SoC cible pour la charge programm\u00e9e<\/strong><\/li>\n<\/ol>\n\n\n\n<ul class=\"wp-block-list\">\n<li>logique \u201cm\u00e9tier\u201d bas\u00e9e sur <strong>besoin \u00e9nerg\u00e9tique J+1<\/strong> (prod vs conso) + param\u00e8tres batterie (capacit\u00e9, rendement, soc_min, marge\u2026)<\/li>\n\n\n\n<li>production d\u2019une <strong>cible prioritaire<\/strong> <code>flow.dess_soc_cible_pct<\/code><\/li>\n\n\n\n<li>possibilit\u00e9 de <strong>for\u00e7age manuel<\/strong> depuis Home Assistant (override total)<\/li>\n<\/ul>\n\n\n\n<ol start=\"3\" class=\"wp-block-list\">\n<li><strong>Piloter la Charge Programm\u00e9e (CP1) dans Venus OS sur la fen\u00eatre 22h\u20136h<\/strong><\/li>\n<\/ol>\n\n\n\n<ul class=\"wp-block-list\">\n<li>activation\/d\u00e9sactivation conditionn\u00e9e par une <strong>autorisation HA<\/strong><\/li>\n\n\n\n<li>calcul d\u2019un <strong>Start\/Duration<\/strong> coh\u00e9rent avec le besoin r\u00e9el (horaires dynamiques)<\/li>\n\n\n\n<li>\u00e9criture des param\u00e8tres CP1 : <strong>Day \/ Start \/ Duration \/ Soc<\/strong> (+ option AllowDischarge si besoin)<\/li>\n\n\n\n<li>log synth\u00e9tique + publication MQTT des sorties utiles (cible SoC, valid CP, dur\u00e9e, heure d\u00e9but\u2026)<\/li>\n<\/ul>\n\n\n\n<p>Ce flow sert donc \u00e0 la fois de <strong>collecte<\/strong> (VRM), de <strong>traitement<\/strong> (calculs), et de <strong>commande<\/strong> (\u00e9criture settings Victron), tout en restant <strong>observable<\/strong> via logs + MQTT.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Le flow Node-RED<\/h1>\n\n\n\n<h2 class=\"wp-block-heading\">Visualisation du flow<\/h2>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"363\" src=\"https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-17-1024x363.png\" alt=\"\" class=\"wp-image-4321\" srcset=\"https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-17-1024x363.png 1024w, https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-17-300x106.png 300w, https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-17-768x272.png 768w, https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-17.png 1125w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"519\" src=\"https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-18-1024x519.png\" alt=\"\" class=\"wp-image-4324\" srcset=\"https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-18-1024x519.png 1024w, https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-18-300x152.png 300w, https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-18-768x389.png 768w, https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-18.png 1049w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"291\" src=\"https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-19-1024x291.png\" alt=\"\" class=\"wp-image-4325\" srcset=\"https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-19-1024x291.png 1024w, https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-19-300x85.png 300w, https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-19-768x218.png 768w, https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-19.png 1038w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1011\" height=\"420\" src=\"https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-20.png\" alt=\"\" class=\"wp-image-4326\" srcset=\"https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-20.png 1011w, https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-20-300x125.png 300w, https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-20-768x319.png 768w\" sizes=\"auto, (max-width: 1011px) 100vw, 1011px\" \/><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Analyse du flow (version 2026)<\/h1>\n\n\n\n<h2 class=\"wp-block-heading\">Bloc \u201cPr\u00e9visions VRM : production PV + consommation J+1 (06h \u2192 22h)\u201d<\/h2>\n\n\n\n<p>Le flow ne se limite plus \u00e0 \u201cadditionner des totaux\u201d : il travaille d\u00e9sormais <strong>\u00e0 la journ\u00e9e suivante (J+1)<\/strong> et sur la plage utile <strong>06:00 \u2192 22:00<\/strong>, ce qui colle au besoin r\u00e9el : <strong>tenir la journ\u00e9e hors HC<\/strong>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Pr\u00e9vision de production PV J+1 (06\u201322)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Un n\u0153ud <strong>VRM API<\/strong> r\u00e9cup\u00e8re la s\u00e9rie <code>records.solar_yield_forecast<\/code> (format typique <code>[[timestamp_ms, Wh], ...]<\/code>).<\/li>\n\n\n\n<li>Fonction <strong>Extraction 6\u201322<\/strong> :\n<ul class=\"wp-block-list\">\n<li>filtre les points correspondant au <strong>jour J+1<\/strong><\/li>\n\n\n\n<li>somme uniquement les valeurs entre <strong>06:00 et 22:00<\/strong><\/li>\n\n\n\n<li>convertit <strong>Wh \u2192 kWh<\/strong> (arrondi \u00e0 <strong>0,1 kWh<\/strong>)<\/li>\n\n\n\n<li>stocke en <code>flow.previ_prod_6_22<\/code> (et optionnellement <code>flow.previ_prod_total<\/code>)<\/li>\n\n\n\n<li>publie sur MQTT : <code>mp2\/dess_remy\/prod_total<\/code> (<strong>QoS 2 + retain<\/strong>)<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>Un <em>change node<\/em> \u201cSave previ_prod\u201d m\u00e9morise la valeur utilis\u00e9e ensuite : <code>flow.previ_prod<\/code>.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Pr\u00e9vision de consommation J+1 (06\u201322)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Un second n\u0153ud <strong>VRM API<\/strong> r\u00e9cup\u00e8re la pr\u00e9vision de consommation <code>records.vrm_consumption_fc<\/code> (s\u00e9rie horaire).<\/li>\n\n\n\n<li>Fonction <strong>Extraction 6\u201322<\/strong> :\n<ul class=\"wp-block-list\">\n<li>filtre <strong>J+1<\/strong><\/li>\n\n\n\n<li>somme uniquement <strong>06:00 \u2192 22:00<\/strong><\/li>\n\n\n\n<li>convertit <strong>Wh \u2192 kWh<\/strong> (arrondi \u00e0 <strong>0,1 kWh<\/strong>)<\/li>\n\n\n\n<li>stocke en <code>flow.conso_j1<\/code><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p>\u00c0 ce stade, Node-RED dispose de deux entr\u00e9es coh\u00e9rentes pour dimensionner la batterie sur J+1 :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Production PV attendue (06\u201322)<\/strong><\/li>\n\n\n\n<li><strong>Consommation attendue (06\u201322)<\/strong><\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Bloc \u201cCalcul SoC cible (logique DoD J+1)\u201d<\/h2>\n\n\n\n<p>C\u2019est la nouveaut\u00e9 structurante du flow.<\/p>\n\n\n\n<p>La fonction <strong>Calcul SoC Cible<\/strong> calcule un SoC cible \u201cm\u00e9tier\u201d \u00e0 partir de :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>flow.conso_j1<\/code> (conso pr\u00e9vue J+1 06\u201322)<\/li>\n\n\n\n<li><code>flow.previ_prod<\/code> (prod pr\u00e9vue J+1 06\u201322)<\/li>\n\n\n\n<li><code>flow.soc<\/code> (SoC actuel)<\/li>\n\n\n\n<li>param\u00e8tres batterie (capacit\u00e9 utile, rendement, soc_min, marge, part r\u00e9seau accept\u00e9e, etc.)<\/li>\n<\/ul>\n\n\n\n<p>Elle produit et stocke notamment :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>flow.dess_soc_cible_pct<\/code> : <strong>SoC cible calcul\u00e9<\/strong><\/li>\n\n\n\n<li><code>flow.dess_dod_pct<\/code> : <strong>DoD requis<\/strong> (indicatif \/ diagnostic)<\/li>\n<\/ul>\n\n\n\n<p>Ce SoC cible devient la <strong>r\u00e9f\u00e9rence prioritaire<\/strong> pour le pilotage CP1.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Bloc \u201cEntr\u00e9es Home Assistant (autorisations et for\u00e7ages)\u201d<\/h2>\n\n\n\n<p>Trois topics MQTT (HA \u2192 Node-RED) permettent de reprendre la main simplement :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>ha\/mp2\/cp\/validcp<\/code> : autorisation globale CP (ON\/OFF)<br>\u2192 stock\u00e9 en <code>global.valid_cp_ess<\/code><\/li>\n\n\n\n<li><code>ha\/mp2\/cp\/forcage100<\/code> : for\u00e7age manuel (ON\/OFF)<br>\u2192 stock\u00e9 en <code>flow.forc100<\/code><\/li>\n\n\n\n<li><code>ha\/mp2\/cp\/niveauforcagecp1<\/code> : cible SoC forc\u00e9e (ex : 80)<br>\u2192 stock\u00e9 en <code>flow.niveauforcp1<\/code><\/li>\n<\/ul>\n\n\n\n<p>Ces entr\u00e9es sont consomm\u00e9es directement par la logique de pilotage CP1.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Bloc \u201cHorloge CP 22h \u2192 6h + horaires dynamiques\u201d<\/h2>\n\n\n\n<p>Le pilotage temporel est maintenant plus propre et plus robuste :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Un <strong>BigTimer<\/strong> d\u00e9finit la fen\u00eatre <strong>22:00 \u2192 06:00<\/strong> (sortie ON\/OFF).<\/li>\n\n\n\n<li>La fonction <strong>H Dynamique V3<\/strong> :\n<ul class=\"wp-block-list\">\n<li>prend la fen\u00eatre BigTimer comme <strong>v\u00e9rit\u00e9<\/strong><\/li>\n\n\n\n<li>calcule <code>hdebut<\/code> (en minutes) en fonction du besoin (<strong>SoC actuel \u2192 SoC cible<\/strong>)<\/li>\n\n\n\n<li>\u00e9crit :\n<ul class=\"wp-block-list\">\n<li><code>flow.horloge<\/code> (0\/1)<\/li>\n\n\n\n<li><code>flow.hdebut<\/code> (minutes)<\/li>\n\n\n\n<li><code>flow.hfin<\/code> (minutes, fin de fen\u00eatre)<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>sort un log clair vers un widget dashboard (\u201cLog H Dyn\u201d)<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p>Recalcul \u201csouple\u201d : un recalcul p\u00e9riodique du SoC est pr\u00e9vu <strong>toutes les 15 min<\/strong> et <strong>uniquement si BigTimer est OFF<\/strong>, afin d\u2019\u00e9viter les oscillations pendant la fen\u00eatre de charge.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Bloc \u201cPilotage CP1 V6 : SoC cible + commande Victron\u201d<\/h2>\n\n\n\n<p>La fonction <strong>Pilotage CP1 V6<\/strong> est le c\u0153ur du pilotage.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Calcul du SoC cible (priorit\u00e9s)<\/h3>\n\n\n\n<p>Ordre de priorit\u00e9 :<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>SoC cible calcul\u00e9 DoD J+1 : <code>flow.dess_soc_cible_pct<\/code> (si valide)<\/li>\n\n\n\n<li>Fallback historique : <code>seuil = 120 - 2.5 \u00d7 prod<\/code><\/li>\n\n\n\n<li>For\u00e7age HA actif (<code>flow.forc100 == on<\/code>) : <code>seuil = flow.niveauforcp1<\/code><\/li>\n<\/ol>\n\n\n\n<p>Puis :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>arrondi au pas de <strong>5%<\/strong><\/li>\n\n\n\n<li>bornage <strong>0\u2013100<\/strong><\/li>\n\n\n\n<li>r\u00e8gle conserv\u00e9e : <strong>&lt; 15% \u21d2 0<\/strong> (si c\u2019est toujours votre choix de fonctionnement)<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Activation \/ d\u00e9sactivation CP<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>si <code>global.valid_cp_ess<\/code> autorise <strong>ET<\/strong> <code>flow.horloge == 1<\/code> \u2192 activation (payload <strong>7<\/strong> = \u201cEvery day\u201d)<\/li>\n\n\n\n<li>sinon \u2192 d\u00e9sactivation (payload <strong>-1<\/strong>)<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Start \/ Duration<\/h3>\n\n\n\n<p>Conversion minutes \u2192 secondes pour alimenter :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>\/Schedule\/Charge\/0\/Start<\/code><\/li>\n\n\n\n<li><code>\/Schedule\/Charge\/0\/Duration<\/code><\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Log synth\u00e9tique<\/h3>\n\n\n\n<p>Log unique et lisible (UI + debug) indiquant :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>SoC actuel<\/li>\n\n\n\n<li>seuil retenu + source (DoD \/ fallback \/ for\u00e7age)<\/li>\n\n\n\n<li>\u00e9tat for\u00e7age<\/li>\n\n\n\n<li>dur\u00e9e HC (HH:MM)<\/li>\n\n\n\n<li>horloge (0\/1)<\/li>\n\n\n\n<li>autorisation HA<\/li>\n\n\n\n<li>valeur effectivement envoy\u00e9e<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Bloc \u201c\u00c9criture Venus OS + MQTT sortant\u201d<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">\u00c9criture des settings dans Venus OS (victron-output-ess)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Validation CP : <code>\/Settings\/CGwacs\/BatteryLife\/Schedule\/Charge\/0\/Day<\/code><\/li>\n\n\n\n<li>Heure d\u00e9but : <code>\/Settings\/CGwacs\/BatteryLife\/Schedule\/Charge\/0\/Start<\/code><\/li>\n\n\n\n<li>Dur\u00e9e : <code>\/Settings\/CGwacs\/BatteryLife\/Schedule\/Charge\/0\/Duration<\/code><\/li>\n\n\n\n<li>Cible SoC : <code>\/Settings\/CGwacs\/BatteryLife\/Schedule\/Charge\/0\/Soc<\/code><\/li>\n\n\n\n<li>(option) Autoconsommation : <code>\/Settings\/CGwacs\/BatteryLife\/Schedule\/Charge\/0\/AllowDischarge<\/code><\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">MQTT sortant (supervision Home Assistant)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>mp2\/dess_remy\/prod_total<\/code> (pr\u00e9vision PV J+1 06\u201322, <strong>QoS2 retain<\/strong>)<\/li>\n\n\n\n<li><code>mp2\/dess_remy\/cible_soc<\/code><\/li>\n\n\n\n<li><code>mp2\/multiplus2\/valide_cp<\/code> (mapping -1\/7 \u2192 on\/off)<\/li>\n\n\n\n<li><code>mp2\/dess_remy\/h_debut<\/code> (HH:MM)<\/li>\n\n\n\n<li><code>mp2\/dess_remy\/duree<\/code> (HH:MM)<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">D\u00e9tails techniques<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Extraction PV J+1 06\u201322 (principe)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>source : <code>records.solar_yield_forecast<\/code><\/li>\n\n\n\n<li>filtre : <strong>jour J+1<\/strong> + plage <strong>06:00\u201322:00<\/strong><\/li>\n\n\n\n<li>conversion : <strong>Wh \u2192 kWh<\/strong> (1 d\u00e9cimale)<\/li>\n\n\n\n<li>stockage : <code>flow.previ_prod_6_22<\/code> (+ <code>flow.previ_prod<\/code> via change node)<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Extraction conso J+1 06\u201322 (principe)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>source : <code>records.vrm_consumption_fc<\/code><\/li>\n\n\n\n<li>filtre : <strong>jour J+1<\/strong> + plage <strong>06:00\u201322:00<\/strong><\/li>\n\n\n\n<li>conversion : <strong>Wh \u2192 kWh<\/strong> (1 d\u00e9cimale)<\/li>\n\n\n\n<li>stockage : <code>flow.conso_j1<\/code><\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Calcul SoC cible + activation CP (principe)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>SoC cible prioritaire : <code>flow.dess_soc_cible_pct<\/code> (DoD J+1)<\/li>\n\n\n\n<li>fallback : <code>120 - 2.5 \u00d7 prod<\/code><\/li>\n\n\n\n<li>for\u00e7age HA : override total<\/li>\n\n\n\n<li>activation CP : uniquement si <code>global.valid_cp_ess<\/code> autorise <strong>ET<\/strong> <code>flow.horloge == 1<\/code><\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Dashboard Node red:<\/h1>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"573\" height=\"567\" src=\"https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-11.png\" alt=\"\" class=\"wp-image-4298\" srcset=\"https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-11.png 573w, https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-11-300x297.png 300w\" sizes=\"auto, (max-width: 573px) 100vw, 573px\" \/><\/figure>\n\n\n\n<h1 class=\"wp-block-heading\">Int\u00e9gration avec Home Assistant<\/h1>\n\n\n\n<p>Home Assistant sert ici de <strong>poste de supervision<\/strong> (dashboards) et de <strong>poste de commande<\/strong> (autorisations \/ for\u00e7ages), via MQTT.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">MQTT \u2192 Home Assistant (t\u00e9l\u00e9m\u00e9trie \/ supervision)<\/h2>\n\n\n\n<p>Le broker MQTT re\u00e7oit et peut exposer dans Home Assistant :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Pr\u00e9vision PV J+1 (06:00 \u2192 22:00)<\/strong>\n<ul class=\"wp-block-list\">\n<li><code>mp2\/dess_remy\/prod_total<\/code> <em>(QoS 2 + retain)<\/em><br>Utile pour affichage, sc\u00e9narios Tempo, et compr\u00e9hension \u201ccharge vs journ\u00e9e\u201d.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>(Optionnel) Pr\u00e9vision conso J+1 (06:00 \u2192 22:00)<\/strong>\n<ul class=\"wp-block-list\">\n<li><code>mp2\/dess_remy\/conso_j1<\/code> <em>(si tu choisis de la publier)<\/em><br>Tr\u00e8s pratique pour visualiser le bilan attendu.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>SoC cible retenu (valeur finale r\u00e9ellement utilis\u00e9e)<\/strong>\n<ul class=\"wp-block-list\">\n<li><code>mp2\/dess_remy\/cible_soc<\/code><br>(incluant priorit\u00e9s : DoD J+1 \u2192 fallback \u2192 for\u00e7age HA)<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>\u00c9tat de charge programm\u00e9e (CP1)<\/strong>\n<ul class=\"wp-block-list\">\n<li><code>mp2\/multiplus2\/valide_cp<\/code> <em>(mapping -1\/7 \u2192 on\/off)<\/em><br>Permet de savoir si CP1 est activ\u00e9e ou non c\u00f4t\u00e9 logique.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Horaires calcul\u00e9s<\/strong>\n<ul class=\"wp-block-list\">\n<li><code>mp2\/dess_remy\/h_debut<\/code> <em>(HH:MM)<\/em><\/li>\n\n\n\n<li><code>mp2\/dess_remy\/duree<\/code> <em>(HH:MM)<\/em><br>Pour lecture imm\u00e9diate dans les dashboards (et diagnostic).<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>(Optionnel) Log synth\u00e9tique<\/strong>\n<ul class=\"wp-block-list\">\n<li><code>mp2\/dess_remy\/log<\/code> <em>(string)<\/em><br>Hyper utile en debug : SoC actuel, cible, for\u00e7age, valid HA, horloge, start\/dur\u00e9e envoy\u00e9s.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Home Assistant \u2192 Node-RED (commande \/ reprise de main)<\/h2>\n\n\n\n<p>Home Assistant peut piloter le flow via trois topics simples :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>ha\/mp2\/cp\/validcp<\/code> : autorisation globale CP (on\/off)<\/li>\n\n\n\n<li><code>ha\/mp2\/cp\/forcage100<\/code> : activation du for\u00e7age manuel (on\/off)<\/li>\n\n\n\n<li><code>ha\/mp2\/cp\/niveauforcagecp1<\/code> : valeur de SoC cible forc\u00e9e (ex : 80)<\/li>\n<\/ul>\n\n\n\n<p>\ud83d\udc49 R\u00e9sultat : <strong>Node-RED calcule et d\u00e9cide<\/strong>, Home Assistant <strong>autorise et force si n\u00e9cessaire<\/strong>, et tout reste <strong>observable<\/strong> via MQTT.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"689\" height=\"559\" src=\"https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-21.png\" alt=\"\" class=\"wp-image-4334\" srcset=\"https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-21.png 689w, https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-21-300x243.png 300w\" sizes=\"auto, (max-width: 689px) 100vw, 689px\" \/><\/figure>\n\n\n\n<p>On ci-dessous le r\u00e9sultat d&rsquo;une charge nocturne, la charge programm\u00e9e d\u00e9marre vers 1:00 pour se terminer avant 6:00<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"554\" src=\"https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-8-1024x554.png\" alt=\"\" class=\"wp-image-4273\" srcset=\"https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-8-1024x554.png 1024w, https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-8-300x162.png 300w, https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-8-768x416.png 768w, https:\/\/domo.rem81.com\/wp-content\/uploads\/2026\/01\/image-8.png 1243w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>Fichier victron.yaml permettant de r\u00e9cuperer les mqtt:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mqtt:\n  sensor:\n  # MQTT\n    - name: \"MP2 SoC Affich\u00e9\"\n      unique_id: mp2_soc_affiche\n      state_topic: \"mp2\/batteries\/soc_affiche\"\n      unit_of_measurement: \"%\"\n      device_class: battery\n      state_class: measurement\n\n    - name: \"MP2 SoC th\u00e9orique Lineaire\"\n      unique_id: mp2_soc_lin_json\n      state_topic: \"mp2\/batteries\/soc_theorique\"\n      unit_of_measurement: \"%\"\n      device_class: battery\n      state_class: measurement\n      value_template: \"{{ value_json.get('SoClin', 0) | float(0) }}\"\n\n    - name: \"MP2 VALIDE CP MQTT\"\n      unique_id: mp2_valide_cp_par_mqtt\n      state_topic: \"mp2\/multiplus2\/valide_cp\"\n\n    - name: \"Capacit\u00e9 Batterie Estim\u00e9e par NR\"\n      state_topic: \"mp2\/batteries\/capacity_est_ah\"\n      unit_of_measurement: \"Ah\"\n      icon: mdi:battery-charging-outline\n      state_class: measurement\n\n      # DESS Remy\n\n    - name: \"MP2 DESS Cible SOC par MQTT\"\n      unique_id: mp2_dess_cible_soc\n      state_topic: \"mp2\/dess_remy\/cible_soc\"\n      unit_of_measurement: '%'\n      device_class: battery\n      state_class: measurement\n\n    - name: \"MP2 DESS Difference Cible-SOC\"\n      unique_id: mp2_difference_cible_soc\n      state_topic: \"mp2\/dess_remy\/diff_soc\"\n      unit_of_measurement: '%'\n      device_class: battery\n      state_class: measurement\n\n    - name: \"MP2 DESS H_Debut DESS Remy\"\n      unique_id: mp2_dess_h_debut\n      state_topic: \"mp2\/dess_remy\/h_debut\"   \n\n    - name: \"MP2 DESS Dur\u00e9e DESS Remy\"\n      unique_id: mp2_dess_duree\n      state_topic: \"mp2\/dess_remy\/duree\"      \n\n    - name: \"MP2 DESS Remy Log Audit\"\n      unique_id: mp2_dess_log_audit\n      state_topic: \"mp2\/dess_remy\/log_audit\"      \n\n    - name: \"MP2 DESS Previ Conso 6-22 J1\"\n      unique_id: mp2_dess_previ_conso_6_22_j1\n      state_topic: \"mp2\/dess_remy\/previ_conso_6-22-J1\"\n      unit_of_measurement: \"kWh\"\n      device_class: \"energy\"\n      state_class: \"measurement\"\n      \n    - name: \"MP2 DESS Previ Prod 6-22 J1\"\n      unique_id: mp2_dess_previ_prod_6_22_j1\n      state_topic: \"mp2\/dess_remy\/previ_prod_6-22-J1\"\n      unit_of_measurement: \"kWh\"\n      device_class: \"energy\"\n      state_class: \"measurement\"<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Palettes utilis\u00e9es<\/h1>\n\n\n\n<p>Pour reproduire ce flow, il faut ces palettes (ou \u00e9quivalents) :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>node-red-contrib-victron<\/strong><br>Lecture\/\u00e9criture des chemins Venus OS (notamment <code>\/Settings\/CGwacs\/BatteryLife\/Schedule\/Charge\/0\/*<\/code>) et int\u00e9gration propre avec l\u2019\u00e9cosyst\u00e8me Victron.<\/li>\n\n\n\n<li><strong>Palette VRM API<\/strong> (n\u0153ud <strong>vrm-api<\/strong>)<br>R\u00e9cup\u00e9ration des s\u00e9ries\/pr\u00e9visions VRM (production PV forecast, consommation forecast), utilis\u00e9es ici en <strong>J+1<\/strong> et sur la plage <strong>06:00 \u2192 22:00<\/strong>.<\/li>\n\n\n\n<li><strong>node-red-contrib-bigtimer<\/strong><br>D\u00e9finition de la fen\u00eatre de charge nocturne <strong>22:00 \u2192 06:00<\/strong> (sortie ON\/OFF servant de \u201cv\u00e9rit\u00e9\u201d pour la logique horaire).<\/li>\n\n\n\n<li><strong>node-red-dashboard<\/strong><br>Widgets de debug\/observabilit\u00e9 : jauges, textes, graphes, log \u201clisible\u201d.<\/li>\n\n\n\n<li><strong>node-red-node-ui-table<\/strong> <em>(optionnel mais pratique)<\/em><br>Affichage tabulaire des valeurs cl\u00e9s (pr\u00e9visions, SoC cible, start\/duration, \u00e9tats HA) pour diagnostiquer rapidement.<\/li>\n\n\n\n<li><strong>node-red-node-mqtt<\/strong> <em>(ou les MQTT nodes int\u00e9gr\u00e9s selon ta version Node-RED)<\/em><br>Entr\u00e9es HA \u2192 Node-RED (autorisations \/ for\u00e7ages) et sorties Node-RED \u2192 HA (t\u00e9l\u00e9m\u00e9trie \/ logs), avec gestion <strong>retain<\/strong> \/ QoS selon tes choix.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Conclusion<\/h1>\n\n\n\n<p>Avec ce flow \u201cDESS R\u00e9my\u201d, je ne me contente plus de remonter des donn\u00e9es : j\u2019ai une cha\u00eene compl\u00e8te <strong>pr\u00e9voir \u2192 d\u00e9cider \u2192 agir<\/strong>, pens\u00e9e pour un usage r\u00e9el au quotidien \u2014 et en particulier pour <strong>EDF Tempo<\/strong>.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Pr\u00e9voir<\/strong> : r\u00e9cup\u00e9ration via VRM des pr\u00e9visions <strong>J+1<\/strong> sur la plage utile <strong>06h \u2192 22h<\/strong> (production PV et consommation), afin de raisonner sur \u201ctenir la journ\u00e9e\u201d plut\u00f4t que sur des totaux bruts.<\/li>\n\n\n\n<li><strong>D\u00e9cider<\/strong> : calcul d\u2019une <strong>cible SoC<\/strong> bas\u00e9e sur un besoin \u00e9nerg\u00e9tique J+1 (logique DoD), avec bornage\/arrondi, et possibilit\u00e9 de <strong>for\u00e7age manuel<\/strong> depuis Home Assistant.<\/li>\n\n\n\n<li><strong>Agir<\/strong> : pilotage automatique de la <strong>Charge Programm\u00e9e CP1<\/strong> dans Venus OS (Day \/ Start \/ Duration \/ Soc), avec des <strong>horaires dynamiques<\/strong> dans la fen\u00eatre 22h\u20136h.<\/li>\n\n\n\n<li><strong>Rester observable<\/strong> : publication MQTT des indicateurs utiles (pr\u00e9visions, cible SoC, \u00e9tat CP, start\/duration, logs), pour dashboards, sc\u00e9narios Tempo, et debug.<\/li>\n<\/ul>\n\n\n\n<p>R\u00e9sultat : une charge nocturne <strong>ajust\u00e9e au besoin<\/strong>, lisible, et facilement contr\u00f4lable depuis Home Assistant (autorisation globale + overrides), tout en gardant Node-RED comme \u201ccerveau\u201d central de l\u2019orchestration.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Annexes<\/h1>\n\n\n\n<h2 class=\"wp-block-heading\">Mini table des topics utilis\u00e9 dans MQTT<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">HA \u2192 Node-RED (commandes \/ for\u00e7ages)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>ha\/mp2\/cp\/validcp<\/code> : autorisation globale CP (on\/off)<\/li>\n\n\n\n<li><code>ha\/mp2\/cp\/forcage100<\/code> : for\u00e7age manuel ON\/OFF<\/li>\n\n\n\n<li><code>ha\/mp2\/cp\/niveauforcagecp1<\/code> : niveau SoC cible forc\u00e9 (ex: 80)<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Node-RED \u2192 HA (supervision)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>mp2\/dess_remy\/prod_total<\/code> : pr\u00e9vision PV J+1 (06\u201322), QoS 2 + retain<\/li>\n\n\n\n<li><code>mp2\/dess_remy\/cible_soc<\/code> : SoC cible retenu<\/li>\n\n\n\n<li><code>mp2\/multiplus2\/valide_cp<\/code> : \u00e9tat CP (on\/off)<\/li>\n\n\n\n<li><code>mp2\/dess_remy\/h_debut<\/code> : heure d\u00e9but CP (HH:MM)<\/li>\n\n\n\n<li><code>mp2\/dess_remy\/duree<\/code> : dur\u00e9e CP (HH:MM)<\/li>\n\n\n\n<li><code>mp2\/dess_remy\/previ_conso_6-22-J1<\/code> : pr\u00e9vision conso J+1 (06\u201322), kWh<\/li>\n\n\n\n<li><code>mp2\/dess_remy\/previ_prod_6-22-J1<\/code> : pr\u00e9vision prod J+1 (06\u201322), kWh<\/li>\n\n\n\n<li><code>mp2\/dess_remy\/log_audit<\/code> : log audit (texte)<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Flux JSON \u00e0 copier\/coller :<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>&#091;\n    {\n        \"id\": \"5fe7408840b1cdf3\",\n        \"type\": \"tab\",\n        \"label\": \"DESS R\u00e9my\",\n        \"disabled\": false,\n        \"info\": \"\",\n        \"env\": &#091;]\n    },\n    {\n        \"id\": \"c64c5ecbe0654837\",\n        \"type\": \"group\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"style\": {\n            \"stroke\": \"#999999\",\n            \"stroke-opacity\": \"1\",\n            \"fill\": \"none\",\n            \"fill-opacity\": \"1\",\n            \"label\": true,\n            \"label-position\": \"nw\",\n            \"color\": \"#a4a4a4\"\n        },\n        \"nodes\": &#091;\n            \"3401e9994f1b41b7\",\n            \"2d3a9307be086227\",\n            \"8e2f3efe43139cbb\",\n            \"309cfd45dc936c74\",\n            \"3771889792a45c9c\",\n            \"30724406b2c3e9a6\",\n            \"fce8b570e7de2979\",\n            \"2f7797d5a59eb37f\",\n            \"f26bc3295655d978\",\n            \"100b50b7dd6436f3\",\n            \"8ef38bc25af154c7\",\n            \"b79542847a8290f1\",\n            \"e8f6758392b2f7d7\",\n            \"a8b2fec6fdd2265b\",\n            \"0980d6b80b2d707a\",\n            \"6c9e2d97af61d80e\",\n            \"4f987019c9af7859\",\n            \"9a02956e8c37d773\",\n            \"d0900f1bce2a4111\",\n            \"59baa58061ee6884\",\n            \"6027c79e01cbefde\",\n            \"19d7eb29a5c60cdc\",\n            \"d9004c05cabbfc69\",\n            \"7914d10c7560953d\"\n        ],\n        \"x\": 34,\n        \"y\": 39,\n        \"w\": 1052,\n        \"h\": 382\n    },\n    {\n        \"id\": \"27c9a9a63c937a72\",\n        \"type\": \"group\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"style\": {\n            \"stroke\": \"#999999\",\n            \"stroke-opacity\": \"1\",\n            \"fill\": \"none\",\n            \"fill-opacity\": \"1\",\n            \"label\": true,\n            \"label-position\": \"nw\",\n            \"color\": \"#a4a4a4\"\n        },\n        \"nodes\": &#091;\n            \"56c0dd1a9c9f59d5\",\n            \"30fc5ca1af8c2544\",\n            \"573c180d02c5fc31\",\n            \"a70c71330cb91fb6\"\n        ],\n        \"x\": 214,\n        \"y\": 1579,\n        \"w\": 572,\n        \"h\": 122\n    },\n    {\n        \"id\": \"f8afda3620427140\",\n        \"type\": \"group\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"style\": {\n            \"stroke\": \"#999999\",\n            \"stroke-opacity\": \"1\",\n            \"fill\": \"none\",\n            \"fill-opacity\": \"1\",\n            \"label\": true,\n            \"label-position\": \"nw\",\n            \"color\": \"#a4a4a4\"\n        },\n        \"nodes\": &#091;\n            \"0af06d70cb632e52\",\n            \"f7ddcf1e407745e9\",\n            \"67bc6865c7406fee\",\n            \"701744df73f35cf2\",\n            \"b70ceb2c221e3bd2\",\n            \"a0b8045a6d72c0eb\",\n            \"071b658e8fc627e4\",\n            \"6d096b019c095341\",\n            \"58133dfe4f341acc\",\n            \"0711c68c660d9c2f\",\n            \"c8d75201e67fa205\"\n        ],\n        \"x\": 54,\n        \"y\": 999,\n        \"w\": 972,\n        \"h\": 282\n    },\n    {\n        \"id\": \"9669903f47819092\",\n        \"type\": \"group\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"style\": {\n            \"stroke\": \"#999999\",\n            \"stroke-opacity\": \"1\",\n            \"fill\": \"none\",\n            \"fill-opacity\": \"1\",\n            \"label\": true,\n            \"label-position\": \"nw\",\n            \"color\": \"#a4a4a4\"\n        },\n        \"nodes\": &#091;\n            \"9cf84313ff660683\",\n            \"4047bb58bd817588\",\n            \"e23bf7ba06f7abf5\",\n            \"38b5d65f62960b22\",\n            \"cbe71d4c64597512\",\n            \"c7bade8283007b6b\",\n            \"31630e5509a5a01c\",\n            \"5eacf6a66623ab28\",\n            \"5ddb73a1d5111255\",\n            \"d9d9937809ad2185\",\n            \"7a72bd69fea8ec17\",\n            \"f5bd2bcbcc8eb176\",\n            \"7b1ac5814db8a9ac\",\n            \"4b84dbc2655e1bcd\",\n            \"ce7397f52af03a02\",\n            \"83b23b5304a18717\",\n            \"4654dd1c8bf3344e\",\n            \"317a0cfb20712290\",\n            \"af7be0afa7182573\",\n            \"5b9f7733139cceb7\",\n            \"51dc07974be1a13d\",\n            \"7987ad4db47bf444\",\n            \"a5d3d6de6b719c0a\",\n            \"c84e616b7483e3a7\",\n            \"0b9a873b13652106\",\n            \"7f9d7f52a663fbbc\"\n        ],\n        \"x\": 34,\n        \"y\": 439,\n        \"w\": 1032,\n        \"h\": 522\n    },\n    {\n        \"id\": \"51e27c869bb5e6f1\",\n        \"type\": \"group\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"style\": {\n            \"stroke\": \"#999999\",\n            \"stroke-opacity\": \"1\",\n            \"fill\": \"none\",\n            \"fill-opacity\": \"1\",\n            \"label\": true,\n            \"label-position\": \"nw\",\n            \"color\": \"#a4a4a4\"\n        },\n        \"nodes\": &#091;\n            \"03c36694b589e110\",\n            \"ab48449b78d44baf\",\n            \"1f1cd907d3eec9a0\",\n            \"800f167bb831e02b\",\n            \"b75a9e08a5dbf223\",\n            \"f32446775a09a1fc\",\n            \"d61899b9b703dfe6\",\n            \"66037dc3f0b544c2\",\n            \"3cac79c890574a77\"\n        ],\n        \"x\": 34,\n        \"y\": 1299,\n        \"w\": 972,\n        \"h\": 242\n    },\n    {\n        \"id\": \"0af06d70cb632e52\",\n        \"type\": \"change\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"f8afda3620427140\",\n        \"name\": \"SOC\",\n        \"rules\": &#091;\n            {\n                \"t\": \"set\",\n                \"p\": \"soc\",\n                \"pt\": \"flow\",\n                \"to\": \"payload\",\n                \"tot\": \"msg\"\n            }\n        ],\n        \"action\": \"\",\n        \"property\": \"\",\n        \"from\": \"\",\n        \"to\": \"\",\n        \"reg\": false,\n        \"x\": 310,\n        \"y\": 1240,\n        \"wires\": &#091;\n            &#091;\n                \"67bc6865c7406fee\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"d0900f1bce2a4111\",\n        \"type\": \"change\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"c64c5ecbe0654837\",\n        \"name\": \"Save previ_prod\",\n        \"rules\": &#091;\n            {\n                \"t\": \"set\",\n                \"p\": \"previ_prod\",\n                \"pt\": \"flow\",\n                \"to\": \"payload\",\n                \"tot\": \"msg\"\n            }\n        ],\n        \"action\": \"\",\n        \"property\": \"\",\n        \"from\": \"\",\n        \"to\": \"\",\n        \"reg\": false,\n        \"x\": 740,\n        \"y\": 140,\n        \"wires\": &#091;\n            &#091;\n                \"9a02956e8c37d773\",\n                \"2d3a9307be086227\",\n                \"d9004c05cabbfc69\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"9cf84313ff660683\",\n        \"type\": \"comment\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"name\": \"Calcul du niveau SOC pour la charge programm\u00e9e\",\n        \"info\": \"\",\n        \"x\": 590,\n        \"y\": 480,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"4047bb58bd817588\",\n        \"type\": \"mqtt out\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"name\": \"Cible SOC\",\n        \"topic\": \"mp2\/dess_remy\/cible_soc\",\n        \"qos\": \"\",\n        \"retain\": \"\",\n        \"respTopic\": \"\",\n        \"contentType\": \"\",\n        \"userProps\": \"\",\n        \"correl\": \"\",\n        \"expiry\": \"\",\n        \"broker\": \"502248144035edbf\",\n        \"x\": 470,\n        \"y\": 860,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"e23bf7ba06f7abf5\",\n        \"type\": \"function\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"name\": \"Pilotage CP1 V8\",\n        \"func\": \"\/\/ ===================================================================\\n\/\/ \ud83d\udd0b Pilotage CP1 V8\\n\/\/ - status_cp1 vient de H_DYN via msg.payload.status_cp1 (7 \/ -1)\\n\/\/ - hdebut\/hfin viennent aussi id\u00e9alement du payload, sinon flow\\n\/\/ - Sorties inchang\u00e9es : &#091;cmd, hDebutS, duree, debug, seuilMsg, autoconsoMsg]\\n\/\/ ===================================================================\\n\\n\/\/ ---------- Helpers ----------\\nfunction hhmmFromMin(mins) {\\n  mins = Number(mins);\\n  if (!Number.isFinite(mins)) return \\\"??:??\\\";\\n  mins = ((Math.round(mins) % 1440) + 1440) % 1440;\\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\\n\/\/ ---------- Entr\u00e9es (flow \/ global) ----------\\nlet soc      = Number(flow.get('soc'));\\nif (!Number.isFinite(soc)) soc = 0;\\n\\nlet prod     = Number(flow.get('previ_prod'));\\nif (!Number.isFinite(prod)) prod = 0;\\n\\nconst havalid = global.get('valid_cp_ess'); \/\/ \\\"on\\\"\/true\/1\\nconst HA_ON = (havalid === \\\"on\\\" || havalid === true || havalid === 1);\\n\\n\/\/ ---------- Lecture STRICTE du payload (sans ?. ) ----------\\nconst payloadObj = (msg &amp;&amp; msg.payload &amp;&amp; typeof msg.payload === \\\"object\\\") ? msg.payload : null;\\n\\nlet status_cp1 = payloadObj ? Number(payloadObj.status_cp1) : NaN;\\nif (!Number.isFinite(status_cp1)) {\\n  node.status({ fill: \\\"red\\\", shape: \\\"ring\\\", text: \\\"ERREUR: payload.status_cp1 absent\\\" });\\n  return &#091;\\n    { payload: -1 },\\n    null,\\n    null,\\n    { payload: \\\"CP1: ERREUR payload.status_cp1 absent\\\" },\\n    null,\\n    { payload: 0 }\\n  ];\\n}\\nstatus_cp1 = (status_cp1 === 7) ? 7 : -1;\\n\\n\/\/ ---------- hdebut\/hfin : priorit\u00e9 payload, sinon flow ----------\\nlet hdeb = payloadObj &amp;&amp; Number.isFinite(Number(payloadObj.hdebut_min)) ? Number(payloadObj.hdebut_min) : Number(flow.get('hdebut'));\\nlet hfin = payloadObj &amp;&amp; Number.isFinite(Number(payloadObj.hfin_min))   ? Number(payloadObj.hfin_min)   : Number(flow.get('hfin'));\\n\\nif (!Number.isFinite(hdeb)) hdeb = 0;\\nif (!Number.isFinite(hfin)) hfin = 0;\\n\\n\/\/ Optionnel: m\u00e9moriser en flow pour coh\u00e9rence globale\\nflow.set('hdebut', hdeb);\\nflow.set('hfin', hfin);\\n\\n\/\/ ---------- Sorties 2\/3 : d\u00e9but (s) + dur\u00e9e (s) ----------\\nlet h_corr = hdeb \/ 60;\\nlet fin    = hfin \/ 60;\\n\\nlet tps = (h_corr > fin)\\n  ? (24 - h_corr + fin) * 3600\\n  : (fin - h_corr) * 3600;\\n\\ntps = Math.round(tps);\\n\\nconst hDebutS = { payload: Math.round(h_corr * 3600) };\\nconst duree   = { payload: tps };\\n\\n\/\/ Format dur\u00e9e HH:MM\\nconst hh = Math.floor(tps \/ 3600);\\nconst mm = Math.floor((tps % 3600) \/ 60);\\nconst duree_hhmm = `${String(hh).padStart(2,'0')}:${String(mm).padStart(2,'0')}`;\\n\\n\/\/ Heures en clair\\nconst hdeb_hhmm = hhmmFromMin(hdeb);\\nconst hfin_hhmm = hhmmFromMin(hfin);\\n\\n\/\/ ---------- Seuil pour info\/debug (tu peux garder ta logique actuelle) ----------\\nlet soc_cible_calc = Number(flow.get('dess_soc_cible_pct'));\\nlet seuil = Number.isFinite(soc_cible_calc) &amp;&amp; soc_cible_calc > 0 ? soc_cible_calc : 0;\\nseuil = Math.round(seuil);\\nif (seuil &lt; 15) seuil = 0;\\nif (seuil > 100) seuil = 100;\\nflow.set(\\\"seuil_soc\\\", seuil);\\nconst seuilMsg = { payload: seuil };\\n\\n\/\/ ---------- Commande CP1 (Sortie 1) ----------\\nlet actif_payload = -1;\\nif (!HA_ON) actif_payload = -1;\\nelse actif_payload = status_cp1;\\n\\n\/\/ m\u00e9morise \u00e9tat\\nflow.set('ess_actif', (actif_payload === 7));\\nflow.set('cp1_status_out', actif_payload); \/\/ optionnel\\n\\n\/\/ ---------- Log ----------\\nconst tag = (!HA_ON) ? \\\"\ud83d\udeab HA OFF\\\" : (actif_payload === 7 ? \\\"\u26a1 CP1 ON\\\" : \\\"\u23f1\ufe0f CP1 OFF\\\");\\n\\nconst debug = {\\n  payload:\\n    `${tag}` +\\n    ` | SoC=${soc.toFixed(1)}%` +\\n    ` | Seuil=${seuil}%` +\\n    ` | Hdeb=${hdeb_hhmm} | Hfin=${hfin_hhmm} | Dur\u00e9e=${duree_hhmm}` +\\n    ` | status_cp1(payload)=${status_cp1}` +\\n    ` | havalid=${havalid}` +\\n    ` | Sortie1=${actif_payload}` +\\n    (payloadObj &amp;&amp; payloadObj.reason ? ` | reason=${payloadObj.reason}` : \\\"\\\")\\n};\\n\\n\/\/ Status Node-RED\\nnode.status({\\n  fill: (!HA_ON) ? \\\"red\\\" : (actif_payload === 7 ? \\\"green\\\" : \\\"blue\\\"),\\n  shape: \\\"dot\\\",\\n  text: `${tag} | ${hdeb_hhmm}\u2192${hfin_hhmm} | out=${actif_payload}`\\n});\\n\\n\/\/ ---------- Sorties ----------\\nreturn &#091;\\n  { payload: actif_payload }, \/\/ 1) status CP1 (7 \/ -1)\\n  hDebutS,                    \/\/ 2) d\u00e9but (s)\\n  duree,                      \/\/ 3) dur\u00e9e (s)\\n  debug,                      \/\/ 4) log dashboard\\n  seuilMsg,                   \/\/ 5) seuil\\n  { payload: 0 }              \/\/ 6) autoconso placeholder\\n];\\n\",\n        \"outputs\": 6,\n        \"timeout\": \"\",\n        \"noerr\": 0,\n        \"initialize\": \"\/\/ Le code ajout\u00e9 ici sera ex\u00e9cut\u00e9 une fois\\n\/\/ \u00e0 chaque d\u00e9marrage du noeud.\\nflow.set('cligno', \\\"off\\\");\",\n        \"finalize\": \"\",\n        \"libs\": &#091;],\n        \"x\": 220,\n        \"y\": 680,\n        \"wires\": &#091;\n            &#091;\n                \"7a72bd69fea8ec17\"\n            ],\n            &#091;\n                \"317a0cfb20712290\"\n            ],\n            &#091;\n                \"4654dd1c8bf3344e\"\n            ],\n            &#091;\n                \"d9d9937809ad2185\",\n                \"7b1ac5814db8a9ac\",\n                \"a5d3d6de6b719c0a\"\n            ],\n            &#091;\n                \"38b5d65f62960b22\",\n                \"4047bb58bd817588\",\n                \"4b84dbc2655e1bcd\",\n                \"7f9d7f52a663fbbc\"\n            ],\n            &#091;\n                \"f5bd2bcbcc8eb176\"\n            ]\n        ],\n        \"outputLabels\": &#091;\n            \"Validation\",\n            \"Heure D\u00e9but\",\n            \"Dur\u00e9e\",\n            \"Log\",\n            \"Ecart SOC\",\n            \"H D\u00e9but\"\n        ]\n    },\n    {\n        \"id\": \"38b5d65f62960b22\",\n        \"type\": \"victron-output-ess\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"service\": \"com.victronenergy.settings\",\n        \"path\": \"\/Settings\/CGwacs\/BatteryLife\/Schedule\/Charge\/0\/Soc\",\n        \"serviceObj\": {\n            \"service\": \"com.victronenergy.settings\",\n            \"name\": \"Venus settings\"\n        },\n        \"pathObj\": {\n            \"path\": \"\/Settings\/CGwacs\/BatteryLife\/Schedule\/Charge\/0\/Soc\",\n            \"type\": \"integer\",\n            \"name\": \"Schedule 1: State of charge (%)\",\n            \"writable\": true\n        },\n        \"initial\": \"\",\n        \"name\": \"Cible SOC \",\n        \"onlyChanges\": false,\n        \"x\": 670,\n        \"y\": 860,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"cbe71d4c64597512\",\n        \"type\": \"victron-output-ess\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"service\": \"com.victronenergy.settings\",\n        \"path\": \"\/Settings\/CGwacs\/BatteryLife\/Schedule\/Charge\/0\/Day\",\n        \"serviceObj\": {\n            \"service\": \"com.victronenergy.settings\",\n            \"name\": \"Venus settings\"\n        },\n        \"pathObj\": {\n            \"path\": \"\/Settings\/CGwacs\/BatteryLife\/Schedule\/Charge\/0\/Day\",\n            \"type\": \"enum\",\n            \"name\": \"Schedule 1: Day\",\n            \"enum\": {\n                \"0\": \"Sunday\",\n                \"1\": \"Monday\",\n                \"2\": \"Tuesday\",\n                \"3\": \"Wednesday\",\n                \"4\": \"Thursday\",\n                \"5\": \"Friday\",\n                \"6\": \"Saturday\",\n                \"7\": \"Every day\",\n                \"8\": \"Weekdays\",\n                \"9\": \"Weekends\",\n                \"11\": \"Monthly\",\n                \"-1\": \"Disabled\"\n            },\n            \"remarks\": \"&lt;p>A negative value means that the schedule has been de-activated.&lt;\/p>\",\n            \"mode\": \"both\"\n        },\n        \"name\": \"Validation CP\",\n        \"onlyChanges\": false,\n        \"x\": 740,\n        \"y\": 620,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"c7bade8283007b6b\",\n        \"type\": \"victron-output-ess\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"service\": \"com.victronenergy.settings\",\n        \"path\": \"\/Settings\/CGwacs\/BatteryLife\/Schedule\/Charge\/0\/Start\",\n        \"serviceObj\": {\n            \"service\": \"com.victronenergy.settings\",\n            \"name\": \"Venus settings\"\n        },\n        \"pathObj\": {\n            \"path\": \"\/Settings\/CGwacs\/BatteryLife\/Schedule\/Charge\/0\/Start\",\n            \"type\": \"integer\",\n            \"name\": \"Schedule 1: Start (seconds after midnight)\",\n            \"writable\": true\n        },\n        \"initial\": \"\",\n        \"name\": \"Heure D\u00e9but\",\n        \"onlyChanges\": false,\n        \"x\": 730,\n        \"y\": 680,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"31630e5509a5a01c\",\n        \"type\": \"victron-output-ess\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"service\": \"com.victronenergy.settings\",\n        \"path\": \"\/Settings\/CGwacs\/BatteryLife\/Schedule\/Charge\/0\/Duration\",\n        \"serviceObj\": {\n            \"service\": \"com.victronenergy.settings\",\n            \"name\": \"Venus settings\"\n        },\n        \"pathObj\": {\n            \"path\": \"\/Settings\/CGwacs\/BatteryLife\/Schedule\/Charge\/0\/Duration\",\n            \"type\": \"integer\",\n            \"name\": \"Schedule 1: Duration (seconds)\",\n            \"writable\": true\n        },\n        \"initial\": \"\",\n        \"name\": \"Dur\u00e9e\",\n        \"onlyChanges\": false,\n        \"x\": 710,\n        \"y\": 740,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"5eacf6a66623ab28\",\n        \"type\": \"mqtt out\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"name\": \"Valid CP\",\n        \"topic\": \"mp2\/multiplus2\/valide_cp\",\n        \"qos\": \"\",\n        \"retain\": \"\",\n        \"respTopic\": \"\",\n        \"contentType\": \"\",\n        \"userProps\": \"\",\n        \"correl\": \"\",\n        \"expiry\": \"\",\n        \"broker\": \"502248144035edbf\",\n        \"x\": 920,\n        \"y\": 620,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"5ddb73a1d5111255\",\n        \"type\": \"mqtt out\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"name\": \"H DEBUT\",\n        \"topic\": \"mp2\/dess_remy\/h_debut\",\n        \"qos\": \"\",\n        \"retain\": \"\",\n        \"respTopic\": \"\",\n        \"contentType\": \"\",\n        \"userProps\": \"\",\n        \"correl\": \"\",\n        \"expiry\": \"\",\n        \"broker\": \"502248144035edbf\",\n        \"x\": 920,\n        \"y\": 680,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"d9d9937809ad2185\",\n        \"type\": \"debug\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"name\": \"debug 62\",\n        \"active\": true,\n        \"tosidebar\": true,\n        \"console\": false,\n        \"tostatus\": true,\n        \"complete\": \"true\",\n        \"targetType\": \"full\",\n        \"statusVal\": \"payload\",\n        \"statusType\": \"auto\",\n        \"x\": 860,\n        \"y\": 820,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"7a72bd69fea8ec17\",\n        \"type\": \"function\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"name\": \"Convert\",\n        \"func\": \"var code = msg.payload;\\n\\n\/\/ Tableau de correspondance code \u2192 texte\\nvar mapper = {\\n    \\\"-1\\\": \\\"off\\\",\\n    \\\"7\\\":  \\\"on\\\"\\n};\\n\\n\/\/ Fonction de mapping\\nfunction extraireTexte(valeur) {\\n    return mapper&#091;valeur.toString()] || \\\"Etat inconnu\\\";\\n}\\n\\n\/\/ Application du mapping\\nvar texte = extraireTexte(code);\\n\\n\/\/ Sortie 1 : image de l'entr\u00e9e (code brut)\\nvar msg1 = { ...msg, payload: code };\\n\\n\/\/ Sortie 2 : texte\\nvar msg2 = { ...msg, payload: texte };\\n\\n\/\/ Affichage dans le status du node\\nnode.status({\\n    fill: (texte === \\\"on\\\") ? \\\"green\\\" : (texte === \\\"off\\\") ? \\\"grey\\\" : \\\"yellow\\\",\\n    shape: \\\"dot\\\",\\n    text: \\\"Etat = \\\" + texte + \\\" (\\\" + code + \\\")\\\"\\n});\\n\\nreturn &#091;msg1, msg2];\\n\",\n        \"outputs\": 2,\n        \"timeout\": \"\",\n        \"noerr\": 0,\n        \"initialize\": \"\",\n        \"finalize\": \"\",\n        \"libs\": &#091;],\n        \"x\": 520,\n        \"y\": 620,\n        \"wires\": &#091;\n            &#091;\n                \"cbe71d4c64597512\"\n            ],\n            &#091;\n                \"5eacf6a66623ab28\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"f5bd2bcbcc8eb176\",\n        \"type\": \"victron-output-ess\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"service\": \"com.victronenergy.settings\",\n        \"path\": \"\/Settings\/CGwacs\/BatteryLife\/Schedule\/Charge\/0\/AllowDischarge\",\n        \"serviceObj\": {\n            \"service\": \"com.victronenergy.settings\",\n            \"name\": \"Venus settings\"\n        },\n        \"pathObj\": {\n            \"path\": \"\/Settings\/CGwacs\/BatteryLife\/Schedule\/Charge\/0\/AllowDischarge\",\n            \"type\": \"enum\",\n            \"name\": \"Schedule 1: Self-consumption above limit\",\n            \"enum\": {\n                \"0\": \"Yes\",\n                \"1\": \"No\"\n            },\n            \"mode\": \"both\"\n        },\n        \"initial\": 0,\n        \"name\": \"Autoconsommation\",\n        \"onlyChanges\": false,\n        \"x\": 490,\n        \"y\": 920,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"7b1ac5814db8a9ac\",\n        \"type\": \"ui_text\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"group\": \"c8a7d12bb7f76f27\",\n        \"order\": 3,\n        \"width\": \"12\",\n        \"height\": \"2\",\n        \"name\": \"\",\n        \"label\": \"Log CP1:\",\n        \"format\": \"{{msg.payload}}\",\n        \"layout\": \"row-spread\",\n        \"className\": \"\",\n        \"style\": false,\n        \"font\": \"\",\n        \"fontSize\": 16,\n        \"color\": \"#000000\",\n        \"x\": 620,\n        \"y\": 800,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"9a02956e8c37d773\",\n        \"type\": \"ui_gauge\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"c64c5ecbe0654837\",\n        \"name\": \"\",\n        \"group\": \"c8a7d12bb7f76f27\",\n        \"order\": 12,\n        \"width\": \"3\",\n        \"height\": \"3\",\n        \"gtype\": \"gage\",\n        \"title\": \"Previ Prod J\",\n        \"label\": \"kWh\",\n        \"format\": \"{{value}}\",\n        \"min\": 0,\n        \"max\": \"30\",\n        \"colors\": &#091;\n            \"#00b500\",\n            \"#e6e600\",\n            \"#ca3838\"\n        ],\n        \"seg1\": \"\",\n        \"seg2\": \"\",\n        \"diff\": false,\n        \"className\": \"\",\n        \"x\": 930,\n        \"y\": 120,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"f7ddcf1e407745e9\",\n        \"type\": \"victron-input-battery\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"f8afda3620427140\",\n        \"service\": \"com.victronenergy.battery\/277\",\n        \"path\": \"\/Soc\",\n        \"serviceObj\": {\n            \"service\": \"com.victronenergy.battery\/277\",\n            \"name\": \"SmartShunt 500A\/50mV\"\n        },\n        \"pathObj\": {\n            \"path\": \"\/Soc\",\n            \"type\": \"float\",\n            \"name\": \"State of charge (%)\"\n        },\n        \"initial\": \"\",\n        \"name\": \"SOC Batteries\",\n        \"onlyChanges\": false,\n        \"roundValues\": \"2\",\n        \"x\": 150,\n        \"y\": 1240,\n        \"wires\": &#091;\n            &#091;\n                \"0af06d70cb632e52\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"4b84dbc2655e1bcd\",\n        \"type\": \"ui_gauge\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"name\": \"\",\n        \"group\": \"c8a7d12bb7f76f27\",\n        \"order\": 10,\n        \"width\": \"3\",\n        \"height\": \"3\",\n        \"gtype\": \"gage\",\n        \"title\": \"SOC Cible\",\n        \"label\": \"%\",\n        \"format\": \"{{value}}\",\n        \"min\": 0,\n        \"max\": \"100\",\n        \"colors\": &#091;\n            \"#00b500\",\n            \"#e6e600\",\n            \"#ca3838\"\n        ],\n        \"seg1\": \"\",\n        \"seg2\": \"\",\n        \"diff\": false,\n        \"className\": \"\",\n        \"x\": 830,\n        \"y\": 900,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"6027c79e01cbefde\",\n        \"type\": \"inject\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"c64c5ecbe0654837\",\n        \"name\": \"6:00->21:00\",\n        \"props\": &#091;\n            {\n                \"p\": \"payload\"\n            },\n            {\n                \"p\": \"topic\",\n                \"vt\": \"str\"\n            }\n        ],\n        \"repeat\": \"\",\n        \"crontab\": \"*\/15 6-20 * * *\",\n        \"once\": true,\n        \"onceDelay\": \"5\",\n        \"topic\": \"\",\n        \"payload\": \"\",\n        \"payloadType\": \"date\",\n        \"x\": 160,\n        \"y\": 120,\n        \"wires\": &#091;\n            &#091;\n                \"4f987019c9af7859\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"a8b2fec6fdd2265b\",\n        \"type\": \"mqtt in\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"c64c5ecbe0654837\",\n        \"name\": \"Forcage CP 100%\",\n        \"topic\": \"ha\/mp2\/cp\/forcage100\",\n        \"qos\": \"2\",\n        \"datatype\": \"auto-detect\",\n        \"broker\": \"502248144035edbf\",\n        \"nl\": false,\n        \"rap\": true,\n        \"rh\": 0,\n        \"inputs\": 0,\n        \"x\": 150,\n        \"y\": 340,\n        \"wires\": &#091;\n            &#091;\n                \"6c9e2d97af61d80e\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"ce7397f52af03a02\",\n        \"type\": \"mqtt in\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"name\": \"Valid ESS MP1\",\n        \"topic\": \"ha\/mp2\/cp\/validcp\",\n        \"qos\": \"2\",\n        \"datatype\": \"auto-detect\",\n        \"broker\": \"502248144035edbf\",\n        \"nl\": false,\n        \"rap\": true,\n        \"rh\": 0,\n        \"inputs\": 0,\n        \"x\": 140,\n        \"y\": 540,\n        \"wires\": &#091;\n            &#091;\n                \"c84e616b7483e3a7\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"8ef38bc25af154c7\",\n        \"type\": \"mqtt in\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"c64c5ecbe0654837\",\n        \"name\": \"Niveau Forcage CP1\",\n        \"topic\": \"ha\/mp2\/cp\/niveauforcagecp1\",\n        \"qos\": \"2\",\n        \"datatype\": \"auto-detect\",\n        \"broker\": \"502248144035edbf\",\n        \"nl\": false,\n        \"rap\": true,\n        \"rh\": 0,\n        \"inputs\": 0,\n        \"x\": 150,\n        \"y\": 280,\n        \"wires\": &#091;\n            &#091;\n                \"0980d6b80b2d707a\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"83b23b5304a18717\",\n        \"type\": \"mqtt out\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"name\": \"Dur\u00e9e\",\n        \"topic\": \"mp2\/dess_remy\/duree\",\n        \"qos\": \"\",\n        \"retain\": \"\",\n        \"respTopic\": \"\",\n        \"contentType\": \"\",\n        \"userProps\": \"\",\n        \"correl\": \"\",\n        \"expiry\": \"\",\n        \"broker\": \"502248144035edbf\",\n        \"x\": 890,\n        \"y\": 740,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"4654dd1c8bf3344e\",\n        \"type\": \"function\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"name\": \"Convert\",\n        \"func\": \"\/\/ Entr\u00e9e attendue : msg.payload = dur\u00e9e en secondes (number ou string)\\n\/\/ Sorties :\\n\/\/ 1) msg.payload = valeur brute d'entr\u00e9e (secondes brutes)\\n\/\/ 2) msg.payload = \\\"HH:MM\\\" (string)\\n\\nlet tps_in = msg.payload;\\nlet tps = Number(tps_in);\\n\\n\/\/ Garde-fous\\nif (!isFinite(tps) || tps &lt; 0) {\\n    node.status({\\n        fill: \\\"red\\\",\\n        shape: \\\"ring\\\",\\n        text: \\\"Dur\u00e9e invalide\\\"\\n    });\\n    return null;\\n}\\n\\n\/\/ Arrondi propre\\ntps = Math.round(tps);\\n\\nconst h = Math.floor(tps \/ 3600);\\nconst m = Math.floor((tps % 3600) \/ 60);\\n\\nconst hhmm = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;\\n\\n\/\/ Sortie 1 : image de l'entr\u00e9e (secondes brutes)\\nconst msg1 = { ...msg, payload: tps_in };\\n\\n\/\/ Sortie 2 : HH:MM\\nconst msg2 = { ...msg, payload: hhmm };\\n\\n\/\/ Status Node-RED\\nnode.status({\\n    fill: \\\"blue\\\",\\n    shape: \\\"dot\\\",\\n    text: `Dur\u00e9e = ${hhmm} (${tps}s)`\\n});\\n\\n\/\/ Optionnel : pour tracer\\n\/\/ msg2.topic = \\\"ton\/topic\/mqtt\/duree_hhmm\\\";\\n\\nreturn &#091;msg1, msg2];\\n\",\n        \"outputs\": 2,\n        \"timeout\": 0,\n        \"noerr\": 0,\n        \"initialize\": \"\",\n        \"finalize\": \"\",\n        \"libs\": &#091;],\n        \"x\": 520,\n        \"y\": 740,\n        \"wires\": &#091;\n            &#091;\n                \"31630e5509a5a01c\"\n            ],\n            &#091;\n                \"83b23b5304a18717\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"317a0cfb20712290\",\n        \"type\": \"function\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"name\": \"Convert\",\n        \"func\": \"\/\/ Entr\u00e9e attendue : msg.payload = dur\u00e9e en secondes (number ou string)\\n\/\/ Sorties :\\n\/\/ 1) msg.payload = valeur brute d'entr\u00e9e (secondes)\\n\/\/ 2) msg.payload = \\\"HH:MM\\\" (string)\\n\\nlet tps_in = msg.payload;\\nlet tps = Number(tps_in);\\n\\n\/\/ Garde-fous\\nif (!isFinite(tps) || tps &lt; 0) {\\n    node.status({\\n        fill: \\\"red\\\",\\n        shape: \\\"ring\\\",\\n        text: \\\"H D\u00e9but invalide\\\"\\n    });\\n    return null;\\n}\\n\\n\/\/ Arrondi propre\\ntps = Math.round(tps);\\n\\nconst h = Math.floor(tps \/ 3600);\\nconst m = Math.floor((tps % 3600) \/ 60);\\n\\nconst hhmm = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;\\n\\n\/\/ Sortie 1 : image de l'entr\u00e9e (secondes brutes)\\nconst msg1 = { ...msg, payload: tps_in };\\n\\n\/\/ Sortie 2 : HH:MM\\nconst msg2 = { ...msg, payload: hhmm };\\n\\n\/\/ Affichage dans le status du node\\nnode.status({\\n    fill: \\\"blue\\\",\\n    shape: \\\"dot\\\",\\n    text: `H D\u00e9but = ${hhmm} (${tps}s)`\\n});\\n\\nreturn &#091;msg1, msg2];\\n\",\n        \"outputs\": 2,\n        \"timeout\": 0,\n        \"noerr\": 0,\n        \"initialize\": \"\",\n        \"finalize\": \"\",\n        \"libs\": &#091;],\n        \"x\": 520,\n        \"y\": 680,\n        \"wires\": &#091;\n            &#091;\n                \"c7bade8283007b6b\"\n            ],\n            &#091;\n                \"5ddb73a1d5111255\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"67bc6865c7406fee\",\n        \"type\": \"function\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"f8afda3620427140\",\n        \"name\": \"SOC ts les 10mn\",\n        \"func\": \"\/\/ Emission SoC toutes les x minutes (sans condition horloge)\\n\/\/ - Au maximum 1 fois toutes les 10 minutes\\n\/\/ - Met \u00e0 jour flow.soc et laisse passer msg\\n\\nconst PERIOD_S = 10 * 60; \/\/ 10 minutes\\n\\nfunction fmtRemain(sec) {\\n  sec = Math.max(0, Math.floor(sec));\\n  const m = Math.floor(sec \/ 60);\\n  const s = sec % 60;\\n  return `${m}m${String(s).padStart(2,'0')}s`;\\n}\\n\\n\/\/ SoC courant (source d\u00e9j\u00e0 en amont ou flow)\\nconst soc = Number(flow.get('soc'));\\nif (!isFinite(soc)) {\\n  node.status({ fill: \\\"red\\\", shape: \\\"ring\\\", text: \\\"SoC invalide \/ absent\\\" });\\n  return null;\\n}\\n\\n\/\/ Rate limit 15 min\\nconst nowTs  = Math.floor(Date.now() \/ 1000);\\nconst lastTs = Number(flow.get('bt_last_ts'));\\n\\nif (isFinite(lastTs)) {\\n  const dt = nowTs - lastTs;\\n  if (dt &lt; PERIOD_S) {\\n    node.status({\\n      fill: \\\"yellow\\\",\\n      shape: \\\"dot\\\",\\n      text: `Attente ${fmtRemain(PERIOD_S - dt)} | SoC=${soc.toFixed(1)}%`\\n    });\\n    return null;\\n  }\\n}\\n\\n\/\/ OK \u2192 m\u00e9morisation\\nflow.set('bt_last_ts', nowTs);\\nflow.set('bt_last_soc', soc); \/\/ optionnel diagnostic\\nflow.set('soc', soc);         \/\/ garantit la dispo pour la suite\\n\\n\/\/ Status OK\\nnode.status({\\n  fill: \\\"green\\\",\\n  shape: \\\"dot\\\",\\n  text: `Emission SoC (15min) | SoC=${soc.toFixed(1)}%`\\n});\\n\\n\/\/ Passe au node suivant (H_dynamique amont BigTimer, etc.)\\nmsg.payload = soc; \/\/ optionnel: utile si le node suivant pr\u00e9f\u00e8re msg.payload\\nreturn msg;\\n\",\n        \"outputs\": 1,\n        \"timeout\": 0,\n        \"noerr\": 0,\n        \"initialize\": \"\",\n        \"finalize\": \"\",\n        \"libs\": &#091;],\n        \"x\": 530,\n        \"y\": 1240,\n        \"wires\": &#091;\n            &#091;\n                \"a0b8045a6d72c0eb\",\n                \"6d096b019c095341\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"701744df73f35cf2\",\n        \"type\": \"ui_text\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"f8afda3620427140\",\n        \"group\": \"c8a7d12bb7f76f27\",\n        \"order\": 2,\n        \"width\": \"12\",\n        \"height\": \"2\",\n        \"name\": \"\",\n        \"label\": \"Log H Dyn:\",\n        \"format\": \"{{msg.payload}}\",\n        \"layout\": \"row-spread\",\n        \"className\": \"\",\n        \"style\": false,\n        \"font\": \"\",\n        \"fontSize\": 16,\n        \"color\": \"#000000\",\n        \"x\": 730,\n        \"y\": 1120,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"2d3a9307be086227\",\n        \"type\": \"vrm-api\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"c64c5ecbe0654837\",\n        \"vrm\": \"5e3a89e5eaeb1cb4\",\n        \"name\": \"VRM Conso J+1\",\n        \"api_type\": \"installations\",\n        \"idUser\": \"\",\n        \"idSite\": \"223181\",\n        \"installations\": \"stats\",\n        \"attribute\": \"vrm_consumption_fc\",\n        \"stats_interval\": \"hours\",\n        \"show_instance\": false,\n        \"stats_start\": \"bot\",\n        \"stats_end\": \"86400\",\n        \"use_utc\": false,\n        \"gps_start\": \"\",\n        \"gps_end\": \"\",\n        \"instance\": \"\",\n        \"vrm_id\": \"\",\n        \"b_max\": \"\",\n        \"tb_max\": \"\",\n        \"fb_max\": \"\",\n        \"tg_max\": \"\",\n        \"fg_max\": \"\",\n        \"b_cycle_cost\": \"\",\n        \"buy_price_formula\": \"\",\n        \"sell_price_formula\": \"\",\n        \"green_mode_on\": true,\n        \"feed_in_possible\": true,\n        \"feed_in_control_on\": true,\n        \"b_goal_hour\": \"\",\n        \"b_goal_SOC\": \"\",\n        \"store_in_global_context\": false,\n        \"verbose\": false,\n        \"x\": 140,\n        \"y\": 220,\n        \"wires\": &#091;\n            &#091;\n                \"8e2f3efe43139cbb\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"3401e9994f1b41b7\",\n        \"type\": \"comment\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"c64c5ecbe0654837\",\n        \"name\": \"R\u00e9cup\u00e9ration de la Production et la Consommation J+1 estim\u00e9e par Victron et Calcul SOC Cible\",\n        \"info\": \"\",\n        \"x\": 550,\n        \"y\": 80,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"309cfd45dc936c74\",\n        \"type\": \"change\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"c64c5ecbe0654837\",\n        \"name\": \"Conso_j1\",\n        \"rules\": &#091;\n            {\n                \"t\": \"set\",\n                \"p\": \"conso_j1\",\n                \"pt\": \"flow\",\n                \"to\": \"payload\",\n                \"tot\": \"msg\"\n            }\n        ],\n        \"action\": \"\",\n        \"property\": \"\",\n        \"from\": \"\",\n        \"to\": \"\",\n        \"reg\": false,\n        \"x\": 540,\n        \"y\": 220,\n        \"wires\": &#091;\n            &#091;\n                \"30724406b2c3e9a6\",\n                \"3771889792a45c9c\"\n            ]\n        ],\n        \"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\"\n    },\n    {\n        \"id\": \"fce8b570e7de2979\",\n        \"type\": \"ui_text\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"c64c5ecbe0654837\",\n        \"group\": \"c8a7d12bb7f76f27\",\n        \"order\": 1,\n        \"width\": \"12\",\n        \"height\": \"2\",\n        \"name\": \"\",\n        \"label\": \"Cible Soc :\",\n        \"format\": \"{{msg.payload}}\",\n        \"layout\": \"row-spread\",\n        \"className\": \"\",\n        \"style\": false,\n        \"font\": \"\",\n        \"fontSize\": 16,\n        \"color\": \"#000000\",\n        \"x\": 970,\n        \"y\": 220,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"b70ceb2c221e3bd2\",\n        \"type\": \"inject\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"f8afda3620427140\",\n        \"name\": \"22:00\",\n        \"props\": &#091;\n            {\n                \"p\": \"payload\"\n            },\n            {\n                \"p\": \"topic\",\n                \"vt\": \"str\"\n            }\n        ],\n        \"repeat\": \"\",\n        \"crontab\": \"00 22 * * *\",\n        \"once\": true,\n        \"onceDelay\": \"2\",\n        \"topic\": \"\",\n        \"payload\": \"\",\n        \"payloadType\": \"date\",\n        \"x\": 160,\n        \"y\": 1120,\n        \"wires\": &#091;\n            &#091;\n                \"6d096b019c095341\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"a0b8045a6d72c0eb\",\n        \"type\": \"ui_gauge\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"f8afda3620427140\",\n        \"name\": \"\",\n        \"group\": \"c8a7d12bb7f76f27\",\n        \"order\": 8,\n        \"width\": \"3\",\n        \"height\": \"3\",\n        \"gtype\": \"gage\",\n        \"title\": \"SOC Calib\",\n        \"label\": \"%\",\n        \"format\": \"{{value}}\",\n        \"min\": 0,\n        \"max\": \"100\",\n        \"colors\": &#091;\n            \"#00b500\",\n            \"#e6e600\",\n            \"#ca3838\"\n        ],\n        \"seg1\": \"\",\n        \"seg2\": \"\",\n        \"diff\": false,\n        \"className\": \"\",\n        \"x\": 810,\n        \"y\": 1240,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"8e2f3efe43139cbb\",\n        \"type\": \"function\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"c64c5ecbe0654837\",\n        \"name\": \"Extraction 6-22h\",\n        \"func\": \"\/\/ ===================================================================\\n\/\/ \ud83d\udd0b Pr\u00e9vision de consommation 6h\u201322h (jour J+1) \u00e0 partir du JSON VRM\\n\/\/ Exemple de donn\u00e9es :\\n\/\/ \\\"records\\\": { \\\"vrm_consumption_fc\\\": &#091;&#091;timestamp, Wh], &#091;timestamp, Wh], ...] }\\n\/\/ ===================================================================\\n\\n\/\/ Lecture du JSON\\nlet data = msg.payload;\\n\\nif (!data || !data.records || !data.records.vrm_consumption_fc) {\\n    node.warn(\\\"\u26a0\ufe0f Donn\u00e9es 'vrm_consumption_fc' manquantes !\\\");\\n    msg.payload = null;\\n    return msg;\\n}\\n\\nlet series = data.records.vrm_consumption_fc;\\nif (!Array.isArray(series) || series.length === 0) {\\n    node.warn(\\\"\u26a0\ufe0f S\u00e9rie 'vrm_consumption_fc' vide ou invalide !\\\");\\n    msg.payload = null;\\n    return msg;\\n}\\n\\n\/\/ ===================================================================\\n\/\/ 1\ufe0f\u20e3 D\u00e9termination du jour du lendemain (en local)\\n\/\/ ===================================================================\\nlet now = new Date();\\nlet tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);\\n\\n\/\/ Fonction de comparaison de date locale\\nfunction sameYMD(a, b) {\\n    return a.getFullYear() === b.getFullYear() &amp;&amp;\\n           a.getMonth() === b.getMonth() &amp;&amp;\\n           a.getDate() === b.getDate();\\n}\\n\\n\/\/ ===================================================================\\n\/\/ 2\ufe0f\u20e3 Somme des valeurs entre 6h00 et 22h00 (jour J+1)\\n\/\/ ===================================================================\\nlet sommeWh = 0;\\nfor (let i = 0; i &lt; series.length; i++) {\\n    let timestamp = series&#091;i]&#091;0];\\n    let valeurWh  = series&#091;i]&#091;1];\\n    if (!timestamp || isNaN(valeurWh)) continue;\\n\\n    let date = new Date(timestamp);\\n    let h = date.getHours();\\n\\n    \/\/ V\u00e9rifie que c'est le jour J+1 et dans la fen\u00eatre 6\u201322h\\n    if (sameYMD(date, tomorrow) &amp;&amp; h >= 6 &amp;&amp; h &lt; 22) {\\n        sommeWh += valeurWh;\\n    }\\n}\\n\\n\/\/ ===================================================================\\n\/\/ 3\ufe0f\u20e3 Conversion Wh \u2192 kWh et arrondi\\n\/\/ ===================================================================\\nlet total_kWh = Math.round((sommeWh \/ 1000) * 10) \/ 10;  \/\/ 1 d\u00e9cimale\\n\\n\/\/ ===================================================================\\n\/\/ 4\ufe0f\u20e3 Sortie et log\\n\/\/ ===================================================================\\nmsg.payload = total_kWh;\\nmsg.topic = \\\"victron\/previ_conso_6_22\\\";\\n\\nflow.set('previ_conso_6_22', total_kWh); \/\/ optionnel : m\u00e9morisation en flow\\n\\nnode.status({ fill: \\\"green\\\", shape: \\\"dot\\\", text: `Pr\u00e9vi conso 6\u201322h = ${total_kWh} kWh` });\\n\\nreturn msg;\\n\",\n        \"outputs\": 1,\n        \"timeout\": 0,\n        \"noerr\": 0,\n        \"initialize\": \"\",\n        \"finalize\": \"\",\n        \"libs\": &#091;],\n        \"x\": 340,\n        \"y\": 220,\n        \"wires\": &#091;\n            &#091;\n                \"309cfd45dc936c74\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"4f987019c9af7859\",\n        \"type\": \"vrm-api\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"c64c5ecbe0654837\",\n        \"vrm\": \"5e3a89e5eaeb1cb4\",\n        \"name\": \"Prod J+1\",\n        \"api_type\": \"installations\",\n        \"idUser\": \"\",\n        \"idSite\": \"223181\",\n        \"installations\": \"stats\",\n        \"attribute\": \"solar_yield_forecast\",\n        \"stats_interval\": \"hours\",\n        \"show_instance\": false,\n        \"stats_start\": \"bot\",\n        \"stats_end\": \"86400\",\n        \"use_utc\": false,\n        \"gps_start\": \"\",\n        \"gps_end\": \"\",\n        \"instance\": \"\",\n        \"vrm_id\": \"\",\n        \"b_max\": \"\",\n        \"tb_max\": \"\",\n        \"fb_max\": \"\",\n        \"tg_max\": \"\",\n        \"fg_max\": \"\",\n        \"b_cycle_cost\": \"\",\n        \"buy_price_formula\": \"\",\n        \"sell_price_formula\": \"\",\n        \"green_mode_on\": true,\n        \"feed_in_possible\": true,\n        \"feed_in_control_on\": true,\n        \"b_goal_hour\": \"\",\n        \"b_goal_SOC\": \"\",\n        \"store_in_global_context\": false,\n        \"verbose\": false,\n        \"x\": 380,\n        \"y\": 140,\n        \"wires\": &#091;\n            &#091;\n                \"59baa58061ee6884\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"59baa58061ee6884\",\n        \"type\": \"function\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"c64c5ecbe0654837\",\n        \"name\": \"Extrac 6:22\",\n        \"func\": \"\/\/ ===================================================================\\n\/\/ \u2600\ufe0f Pr\u00e9vision PV J+1 entre 06:00 et 22:00 (kWh)\\n\/\/ Source : records.solar_yield_forecast = &#091;&#091;timestamp_ms, Wh], ...]\\n\/\/ JSON VRM tel que : msg.payload.records.solar_yield_forecast\\n\/\/ ===================================================================\\n\\nconst data = msg.payload;\\n\\nif (!data?.records?.solar_yield_forecast) {\\n  node.warn(\\\"\u26a0\ufe0f Donn\u00e9es 'solar_yield_forecast' manquantes !\\\");\\n  node.status({ fill: \\\"red\\\", shape: \\\"ring\\\", text: \\\"PV forecast absent\\\" });\\n  msg.payload = null;\\n  return msg;\\n}\\n\\nconst series = data.records.solar_yield_forecast;\\nif (!Array.isArray(series) || series.length === 0) {\\n  node.warn(\\\"\u26a0\ufe0f S\u00e9rie 'solar_yield_forecast' vide !\\\");\\n  node.status({ fill: \\\"red\\\", shape: \\\"ring\\\", text: \\\"PV forecast vide\\\" });\\n  msg.payload = null;\\n  return msg;\\n}\\n\\n\/\/ -------------------------------------------------------------------\\n\/\/ 1) D\u00e9termination du jour J+1 (local)\\n\/\/ -------------------------------------------------------------------\\nconst now = new Date();\\nconst tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);\\n\\nfunction sameYMD(a, b) {\\n  return a.getFullYear() === b.getFullYear() &amp;&amp;\\n         a.getMonth() === b.getMonth() &amp;&amp;\\n         a.getDate() === b.getDate();\\n}\\n\\n\/\/ -------------------------------------------------------------------\\n\/\/ 2) Somme Wh entre 06:00 et 22:00 sur J+1\\n\/\/ -------------------------------------------------------------------\\nlet sumWh = 0;\\nlet kept = 0;\\n\\nfor (const row of series) {\\n  if (!Array.isArray(row) || row.length &lt; 2) continue;\\n\\n  const ts = row&#091;0];\\n  const wh = Number(row&#091;1]);\\n\\n  if (!Number.isFinite(ts) || !Number.isFinite(wh)) continue;\\n\\n  const d = new Date(ts);      \/\/ ts en millisecondes => OK\\n  const h = d.getHours();\\n\\n  if (sameYMD(d, tomorrow) &amp;&amp; h >= 6 &amp;&amp; h &lt; 22) {\\n    sumWh += wh;\\n    kept++;\\n  }\\n}\\n\\n\/\/ -------------------------------------------------------------------\\n\/\/ 3) Conversion Wh -> kWh (1 d\u00e9cimale)\\n\/\/ -------------------------------------------------------------------\\nconst kWh_6_22 = Math.round((sumWh \/ 1000) * 10) \/ 10;\\n\\n\/\/ Optionnel : total jour depuis totals\\nconst totalWh = Number(data?.totals?.solar_yield_forecast);\\nconst kWh_total = Number.isFinite(totalWh)\\n  ? Math.round((totalWh \/ 1000) * 10) \/ 10\\n  : null;\\n\\n\/\/ -------------------------------------------------------------------\\n\/\/ 4) Sortie + stockage flow + status\\n\/\/ -------------------------------------------------------------------\\nflow.set(\\\"previ_prod_6_22\\\", kWh_6_22);   \/\/ &lt;- cl\u00e9 \u00e0 utiliser dans ton calcul DoD\\nif (kWh_total !== null) flow.set(\\\"previ_prod_total\\\", kWh_total);\\n\\nnode.status({\\n  fill: \\\"green\\\",\\n  shape: \\\"dot\\\",\\n  text: `PV J+1 6\u201322 = ${kWh_6_22} kWh` + (kWh_total !== null ? ` (total=${kWh_total})` : \\\"\\\") + ` | pts=${kept}`\\n});\\n\\nmsg.payload = kWh_6_22;\\nmsg.topic = \\\"victron\/solar_yield_forecast_6_22\\\";\\n\\nreturn msg;\\n\",\n        \"outputs\": 1,\n        \"timeout\": 0,\n        \"noerr\": 0,\n        \"initialize\": \"\",\n        \"finalize\": \"\",\n        \"libs\": &#091;],\n        \"x\": 550,\n        \"y\": 140,\n        \"wires\": &#091;\n            &#091;\n                \"d0900f1bce2a4111\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"30724406b2c3e9a6\",\n        \"type\": \"function\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"c64c5ecbe0654837\",\n        \"name\": \"Calcul Soc Cible V8\",\n        \"func\": \"\/\/ ============================================\\n\/\/ Calcul SOC Cible V7\\n\/\/ DESS perso - Calcul SoC cible (sans fen\u00eatre)\\n\/\/ - SoC utile uniquement entre SOC_USE_MIN et 100%\\n\/\/ - Ajout SOC_MORNING_BUFFER_PCT : plancher cible = SOC_USE_MIN + buffer\\n\/\/ - For\u00e7age (forc100=on) archi prioritaire : nfcp1 devient la cible stock\u00e9e\\n\/\/ Sorties :\\n\/\/ 1) payload objet (dashboard \/ MQTT)\\n\/\/ 2) log texte lisible (dashboard)\\n\/\/ 3) log texte HA (fichier)\\n\/\/ ============================================\\n\\n\/\/ -------- PARAMETRES BATTERIE --------\\nconst batt_nom_kWh = 12.0;       \/\/ capacit\u00e9 utile r\u00e9elle (sur SOC_USE_MIN->100)\\nconst SOC_USE_MIN  = 21.0;       \/\/ en dessous => \u00e9nergie inutilisable (ex: 21 ou 30)\\nconst SOC_USE_MAX  = 100.0;\\nconst eta          = 0.92;       \/\/ rendement global\\n\\n\/\/ \u2705 R\u00e9serve matin (plancher cible AU-DESSUS du SoC utile)\\nconst SOC_MORNING_BUFFER_PCT = 18.0;   \/\/ ex: 15 => cible mini = SOC_USE_MIN + 15\\n\\n\/\/ -------- MARGES ----------\\nconst marge_kWh     = 2.5; \/\/0.5\\nconst marge_soc_pct = 2.0;\\n\\n\/\/ -------- UTILS ----------\\nfunction clamp(x, a, b) { return Math.max(a, Math.min(b, x)); }\\nfunction ok(n) { return Number.isFinite(n); }\\nfunction hhmm(d) {\\n  return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;\\n}\\nfunction ymd(d) {\\n  const y = d.getFullYear();\\n  const m = String(d.getMonth() + 1).padStart(2,'0');\\n  const da = String(d.getDate()).padStart(2,'0');\\n  return `${y}-${m}-${da}`;\\n}\\n\\n\/\/ -------- TIME ----------\\nconst now = new Date();\\nconst today = ymd(now);\\n\\n\/\/ -------- FORCAGE (PRIORITAIRE) ----------\\nconst f100  = flow.get('forc100');                 \/\/ \\\"on\\\"\/\\\"off\\\" (ou bool)\\nconst nfcp1 = Number(flow.get('niveauforcp1'));    \/\/ ex: 100, 85...\\nconst isF100 = (f100 === \\\"on\\\" || f100 === true || f100 === 1 || f100 === \\\"1\\\");\\n\\n\/\/ ----------- ENTREES -----------\\nconst conso_kWh = Number(flow.get('conso_j1'));     \/\/ kWh 06->22\\nconst pv_kWh    = Number(flow.get('previ_prod'));  \/\/ kWh 06->22\\nlet socNow      = Number(flow.get('soc'));         \/\/ %\\n\\n\/\/ Cible courante (veille) pour status en cas d'erreur\\nconst cibleVeille = Number(flow.get('dess_soc_cible_pct'));\\nconst cibleTxt = Number.isFinite(cibleVeille) ? `${cibleVeille.toFixed(1)}%` : `?%`;\\n\\n\/\/ ---- Normalisation SoC ----\\nif (!ok(socNow)) socNow = 0;\\nsocNow = clamp(socNow, 0, 100);\\n\\n\/\/ SoC \\\"utile\\\" pour les calculs \u00e9nergie (en dessous du mini => mini)\\nconst socNow_util = clamp(socNow, SOC_USE_MIN, SOC_USE_MAX);\\n\\n\/\/ -------- CAPACITES utiles (SOC_USE_MIN..100) ----------\\nconst socMin = SOC_USE_MIN;\\nconst socMax = SOC_USE_MAX;\\n\\n\/\/ \u2705 Plancher de cible (r\u00e9serve matin)\\nconst soc_floor_target = clamp(socMin + SOC_MORNING_BUFFER_PCT, socMin, socMax);\\n\\n\/\/ Capacit\u00e9 utile sur la plage &#091;socMin..socMax]\\nconst batt_utile_kWh =\\n  Math.max(0.01, batt_nom_kWh * (socMax - socMin) \/ 100) * eta;\\n\\n\/\/ Energie disponible actuelle au-dessus du plancher utile\\nconst fracNow = clamp((socNow_util - socMin) \/ (socMax - socMin), 0, 1);\\nconst energie_dispo_kWh = fracNow * batt_utile_kWh;\\n\\n\/\/ -------- CALCUL CIBLE AUTO (si entr\u00e9es OK) ----------\\nlet deficit_kWh = null;\\nlet dod = null;\\nlet soc_cible_calc = null;\\nlet energie_a_charger_kWh = null;\\nlet soc_a_charger_pct = null;\\n\\nconst inputsOk = &#091;conso_kWh, pv_kWh].every(ok);\\n\\n\/\/ Mode FORCE : on ne d\u00e9pend pas des entr\u00e9es, mais on peut quand m\u00eame calculer \u00e0 titre info si OK\\nif (inputsOk) {\\n  deficit_kWh = (conso_kWh - pv_kWh) + marge_kWh;\\n  deficit_kWh = Math.max(0, deficit_kWh);\\n\\n  dod = clamp(deficit_kWh \/ batt_utile_kWh, 0, 1);\\n\\n  soc_cible_calc = socMin + dod * (socMax - socMin);\\n  soc_cible_calc = clamp(soc_cible_calc + marge_soc_pct, socMin, socMax);\\n\\n  \/\/ \u2705 applique le plancher \\\"r\u00e9serve matin\\\"\\n  soc_cible_calc = Math.max(soc_cible_calc, soc_floor_target);\\n\\n  \/\/ Energie cible correspondante\\n  const fracCible = clamp((soc_cible_calc - socMin) \/ (socMax - socMin), 0, 1);\\n  const energie_cible_kWh = fracCible * batt_utile_kWh;\\n\\n  energie_a_charger_kWh = Math.max(0, energie_cible_kWh - energie_dispo_kWh);\\n\\n  \/\/ SoC \u00e0 charger en points \\\"utiles\\\"\\n  soc_a_charger_pct = clamp(soc_cible_calc - socNow_util, 0, socMax - socMin);\\n}\\n\\n\/\/ -------- CIBLE STOCK\u00c9E (veille) - FORCAGE PRIORITAIRE ----------\\nlet soc_cible_stockee = null;\\n\\nif (isF100) {\\n  \/\/ For\u00e7age archi prioritaire\\n  const forced = ok(nfcp1) ? nfcp1 : 100;\\n  soc_cible_stockee = clamp(forced, 0, 100);\\n\\n  \/\/ On stocke direct la cible forc\u00e9e (m\u00eame si entr\u00e9es KO)\\n  flow.set('dess_soc_cible_pct', Number(soc_cible_stockee.toFixed(1)));\\n\\n  \/\/ DoD optionnel si inputs OK\\n  if (ok(dod)) flow.set('dess_dod_pct', Number((dod * 100).toFixed(1)));\\n\\n} else {\\n  \/\/ AUTO : n\u00e9cessite entr\u00e9es OK\\n  if (!inputsOk) {\\n    node.status({ fill: \\\"red\\\", shape: \\\"ring\\\", text: `Entrees manquantes | cible(veille)=${cibleTxt}` });\\n\\n    const txt =\\n      `DESS_PERSO @ ${hhmm(now)} | ERREUR entrees` +\\n      ` | conso_j1=${flow.get('conso_j1')}` +\\n      ` | pv=${flow.get('previ_prod')}` +\\n      ` | soc=${flow.get('soc')}` +\\n      ` | cible_veille=${cibleTxt}`;\\n\\n    return &#091; null, { payload: txt }, { payload: txt } ];\\n  }\\n\\n  soc_cible_stockee = Number(soc_cible_calc.toFixed(1));\\n\\n  \/\/ Mise \u00e0 jour cible \\\"veille\\\"\\n  flow.set('dess_soc_cible_pct', soc_cible_stockee);\\n  flow.set('dess_dod_pct', Number((dod * 100).toFixed(1)));\\n}\\n\\n\/\/ Trace dernier calcul\\nflow.set('dess_soc_calc_ts', now.toISOString());\\n\\n\/\/ -------- SORTIE 1 : OBJET ----------\\nconst payload = {\\n  \/\/ Inputs\\n  conso_kWh: ok(conso_kWh) ? Number(conso_kWh.toFixed(2)) : null,\\n  pv_kWh: ok(pv_kWh) ? Number(pv_kWh.toFixed(2)) : null,\\n  deficit_kWh: ok(deficit_kWh) ? Number(deficit_kWh.toFixed(2)) : null,\\n\\n  \/\/ SoC\\n  soc_now_pct: Number(socNow.toFixed(1)),\\n  soc_now_util_pct: Number(socNow_util.toFixed(1)),\\n  soc_use_min_pct: socMin,\\n  soc_use_max_pct: socMax,\\n\\n  \/\/ \u2705 Buffer \/ plancher\\n  soc_morning_buffer_pct: SOC_MORNING_BUFFER_PCT,\\n  soc_floor_target_pct: Number(soc_floor_target.toFixed(1)),\\n\\n  \/\/ Cibles\\n  soc_cible_calc_pct: ok(soc_cible_calc) ? Number(soc_cible_calc.toFixed(1)) : null,\\n  soc_cible_stockee_pct: Number(soc_cible_stockee.toFixed(1)),\\n\\n  \/\/ \u00c9nergies\\n  batt_utile_kWh: Number(batt_utile_kWh.toFixed(2)),\\n  energie_dispo_kWh: Number(energie_dispo_kWh.toFixed(2)),\\n  energie_a_charger_kWh: ok(energie_a_charger_kWh) ? Number(energie_a_charger_kWh.toFixed(2)) : null,\\n  soc_a_charger_pct: ok(soc_a_charger_pct) ? Number(soc_a_charger_pct.toFixed(1)) : null,\\n\\n  \/\/ Meta\\n  dod_pct: ok(dod) ? Number((dod * 100).toFixed(1)) : null,\\n  forc100: (f100 !== undefined &amp;&amp; f100 !== null ? f100 : null),\\n  niveauforcp1: (ok(nfcp1) ? nfcp1 : null),\\n  mode: isF100 ? \\\"FORCE\\\" : \\\"AUTO\\\",\\n  calc_day: today,\\n  calc_time: hhmm(now)\\n};\\n\\nmsg.payload = payload;\\n\\n\/\/ -------- LOGS ----------\\nconst deltaSoc = payload.soc_cible_stockee_pct - payload.soc_now_pct;\\nconst lockTxt = isF100 ? `FORCE(${payload.soc_cible_stockee_pct}%)` : `AUTO(${payload.soc_cible_stockee_pct}%)`;\\n\\nconst logDash =\\n  `SOC Cible @ ${hhmm(now)}` +\\n  ` | SoC=${payload.soc_now_pct}%\u2192${payload.soc_cible_stockee_pct}% (\u0394=${deltaSoc.toFixed(1)}%)` +\\n  ` | SoCutile>=${SOC_USE_MIN}% (SoCutil=${payload.soc_now_util_pct}%)` +\\n  ` | floor=${payload.soc_floor_target_pct}% (buffer=${SOC_MORNING_BUFFER_PCT}%)` +\\n  (payload.deficit_kWh !== null ? ` | deficit=${payload.deficit_kWh}kWh` : ``) +\\n  (payload.energie_a_charger_kWh !== null ? ` | a_charger=${payload.energie_a_charger_kWh}kWh` : ``) +\\n  ` | mode=${lockTxt}` +\\n  ` | day=${today}`;\\n\\nconst logHa =\\n  `SOC Cible @ ${hhmm(now)}` +\\n  ` | mode=${lockTxt}` +\\n  ` | SoC=${payload.soc_now_pct}% (SoCutil=${payload.soc_now_util_pct}%, min=${SOC_USE_MIN}%)` +\\n  ` | floor=${payload.soc_floor_target_pct}% (buffer=${SOC_MORNING_BUFFER_PCT}%)` +\\n  ` | cible_stockee=${payload.soc_cible_stockee_pct}%` +\\n  (payload.soc_cible_calc_pct !== null ? ` | cible_calc=${payload.soc_cible_calc_pct}%` : ``) +\\n  (payload.conso_kWh !== null ? ` | conso_J1=${payload.conso_kWh}kWh` : ``) +\\n  (payload.pv_kWh !== null ? ` | pv_J1=${payload.pv_kWh}kWh` : ``) +\\n  (payload.deficit_kWh !== null ? ` | deficit=${payload.deficit_kWh}kWh` : ``) +\\n  ` | batt_utile=${payload.batt_utile_kWh}kWh` +\\n  ` | dispo=${payload.energie_dispo_kWh}kWh` +\\n  (payload.energie_a_charger_kWh !== null ? ` | a_charger=${payload.energie_a_charger_kWh}kWh` : ``) +\\n  (payload.soc_a_charger_pct !== null ? ` | soc_a_charger=${payload.soc_a_charger_pct}%` : ``) +\\n  (payload.dod_pct !== null ? ` | DoD=${payload.dod_pct}%` : ``) +\\n  ` | forc100=${String(f100)}` +\\n  ` | nfcp1=${ok(nfcp1) ? nfcp1 : \\\"NaN\\\"}` +\\n  ` | day=${today}`;\\n\\n\/\/ -------- STATUS ----------\\nconst need = (payload.energie_a_charger_kWh !== null &amp;&amp; payload.energie_a_charger_kWh > 0.05);\\nnode.status({\\n  fill: isF100 ? \\\"red\\\" : (need ? \\\"yellow\\\" : \\\"green\\\"),\\n  shape: \\\"dot\\\",\\n  text: `SoC cible=${payload.soc_cible_stockee_pct}% (${payload.mode}) | floor=${payload.soc_floor_target_pct}% | SoC=${payload.soc_now_pct}% | ${hhmm(now)}`\\n});\\n\\n\/\/ -------- RETOUR ----------\\nreturn &#091;\\n  msg,\\n  { payload: logDash },\\n  { payload: logHa }\\n];\\n\",\n        \"outputs\": 3,\n        \"timeout\": 0,\n        \"noerr\": 0,\n        \"initialize\": \"\",\n        \"finalize\": \"\",\n        \"libs\": &#091;],\n        \"x\": 760,\n        \"y\": 220,\n        \"wires\": &#091;\n            &#091;],\n            &#091;\n                \"fce8b570e7de2979\"\n            ],\n            &#091;\n                \"7914d10c7560953d\",\n                \"2f7797d5a59eb37f\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"30fc5ca1af8c2544\",\n        \"type\": \"mqtt out\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"27c9a9a63c937a72\",\n        \"name\": \"Log MQTT->HA\",\n        \"topic\": \"mp2\/dess_remy\/log_audit\",\n        \"qos\": \"2\",\n        \"retain\": \"true\",\n        \"respTopic\": \"\",\n        \"contentType\": \"\",\n        \"userProps\": \"\",\n        \"correl\": \"\",\n        \"expiry\": \"\",\n        \"broker\": \"502248144035edbf\",\n        \"x\": 520,\n        \"y\": 1660,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"2f7797d5a59eb37f\",\n        \"type\": \"link out\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"c64c5ecbe0654837\",\n        \"name\": \"Log Audit\",\n        \"mode\": \"link\",\n        \"links\": &#091;\n            \"573c180d02c5fc31\"\n        ],\n        \"x\": 945,\n        \"y\": 280,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"573c180d02c5fc31\",\n        \"type\": \"link in\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"27c9a9a63c937a72\",\n        \"name\": \"Log In\",\n        \"links\": &#091;\n            \"2f7797d5a59eb37f\",\n            \"cfd34293e830d3cd\",\n            \"071b658e8fc627e4\",\n            \"bb69e83f2e660d25\",\n            \"a5d3d6de6b719c0a\",\n            \"f32446775a09a1fc\"\n        ],\n        \"x\": 315,\n        \"y\": 1660,\n        \"wires\": &#091;\n            &#091;\n                \"30fc5ca1af8c2544\",\n                \"a70c71330cb91fb6\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"071b658e8fc627e4\",\n        \"type\": \"link out\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"f8afda3620427140\",\n        \"name\": \"link out 4\",\n        \"mode\": \"link\",\n        \"links\": &#091;\n            \"573c180d02c5fc31\"\n        ],\n        \"x\": 755,\n        \"y\": 1180,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"6d096b019c095341\",\n        \"type\": \"function\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"f8afda3620427140\",\n        \"name\": \"H Dynamique V7\",\n        \"func\": \"\/\/ =====================================================\\n\/\/ H_DYN v6.2 (SANS BigTimer) - Fen\u00eatre s\u00e9curit\u00e9 PRIORITAIRE START_FLOOR\u2192END_FIXED\\n\/\/ - En dehors de la fen\u00eatre s\u00e9curit\u00e9 : out=-1 + purge fen\u00eatre\\n\/\/ - Dans la fen\u00eatre s\u00e9curit\u00e9 :\\n\/\/    - si CP1 r\u00e9ellement ON (l_status==7 ou -7) => GEL (pas de recalcul d'heures)\\n\/\/         * MAIS: si SoC>=cible => out=-1 (on coupe), sinon out=7 seulement si nowInWin_mem=true\\n\/\/    - sinon => calcule hdebut (hfin fixe END_FIXED), m\u00e9morise flow.hdebut\/hfin,\\n\/\/              et sort 7 uniquement si nowInWin=true, sinon -1\\n\/\/ Sorties : &#091;payload_obj, log_txt]\\n\/\/ =====================================================\\n\\n\/\/ --- Param\u00e8tres (identiques \u00e0 votre algo) ---\\nconst CAP_AH   = 300;    \/\/ capacit\u00e9 totale (Ah)\\nconst I_AVG    = 40;     \/\/ courant moyen utile (A)\\nconst ABS_MIN  = 60;     \/\/ marge absorption\/synchro (min)\\nconst MIN_WIN  = 15;     \/\/ fen\u00eatre minimale (min)\\nconst MAX_WIN  = 8 * 60; \/\/ garde-fou (min)\\nconst STEP     = 5;      \/\/ pas minutes\\n\\n\/\/ Fen\u00eatre s\u00e9curit\u00e9\/plancher + fin fixe\\nconst START_FLOOR_MIN = 22 * 60 + 0;   \/\/ 22:00\\nconst END_FIXED_MIN   = 5 * 60 + 45;  \/\/ 5:45\\n\\n\/\/ Deadband (optionnel). 0 => d\u00e9sactiv\u00e9\\nconst DELTA_DEADBAND_PCT = 0.0;\\n\\n\/\/ --- Utils ---\\nfunction clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }\\nfunction normMin(mins) {\\n  mins = Number(mins);\\n  if (!Number.isFinite(mins)) mins = 0;\\n  return ((Math.round(mins) % 1440) + 1440) % 1440;\\n}\\nfunction hhmm(mins) {\\n  mins = normMin(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\/\/ Fen\u00eatre &#091;start..end) avec gestion minuit\\nfunction isInWindow(nowMin, startMin, endMin) {\\n  nowMin   = normMin(nowMin);\\n  startMin = normMin(startMin);\\n  endMin   = normMin(endMin);\\n\\n  if (startMin === endMin) return false;\\n  if (startMin &lt; endMin) return (nowMin >= startMin) &amp;&amp; (nowMin &lt; endMin);\\n  return (nowMin >= startMin) || (nowMin &lt; endMin);\\n}\\n\/\/ Fen\u00eatre s\u00e9curit\u00e9 START_FLOOR\u2192END_FIXED (peut traverser minuit)\\nfunction isInSafety(nowMin) {\\n  nowMin = normMin(nowMin);\\n  return isInWindow(nowMin, START_FLOOR_MIN, END_FIXED_MIN);\\n}\\n\\n\/\/ --- Temps courant ---\\nconst now = new Date();\\nconst nowMin  = now.getHours() * 60 + now.getMinutes();\\nconst now_hhmm = hhmm(nowMin);\\nconst inSafety = isInSafety(nowMin);\\n\\n\/\/ --- Entr\u00e9es SoC \/ cible ---\\nlet soc = Number(flow.get('soc'));\\nif (!Number.isFinite(soc)) soc = 0;\\nsoc = clamp(soc, 0, 100);\\n\\nlet target = Number(flow.get('dess_soc_cible_pct'));\\nif (!Number.isFinite(target)) target = 100;\\ntarget = clamp(target, 0, 100);\\n\\n\/\/ Besoin en % (toujours interpr\u00e9t\u00e9 comme \\\"cible - soc\\\")\\nconst need_pct_signed = target - soc;                 \/\/ sign\u00e9\\nconst need_pct = Math.max(0, need_pct_signed);        \/\/ >= 0\\nconst soc_ge_cible = (need_pct &lt;= 0);                 \/\/ SoC >= cible\\n\\n\/\/ Deadband\\nconst inDeadband = (DELTA_DEADBAND_PCT > 0) &amp;&amp; (need_pct > 0) &amp;&amp; (need_pct &lt; DELTA_DEADBAND_PCT);\\n\\n\/\/ --- Status CP1 r\u00e9el (retour Victron) ---\\n\/\/ 7 = CP1 ON (r\u00e9el). -7 = forc\u00e9 depuis Victron (consid\u00e9r\u00e9 ON c\u00f4t\u00e9 \\\"r\u00e9el\\\")\\nlet l_status = Number(flow.get('l_status_cp1'));\\nif (!Number.isFinite(l_status)) l_status = -1;\\nconst cp1ReallyOn = (l_status === 7 || l_status === -7);\\n\\n\/\/ --- Fen\u00eatre m\u00e9moris\u00e9e ---\\nlet hdeb_mem = Number(flow.get('hdebut'));\\nlet hfin_mem = Number(flow.get('hfin'));\\nif (!Number.isFinite(hdeb_mem)) hdeb_mem = null;\\nif (!Number.isFinite(hfin_mem)) hfin_mem = END_FIXED_MIN;\\n\\n\/\/ =====================================================\\n\/\/ 0) PRIORIT\u00c9 ABSOLUE : hors fen\u00eatre s\u00e9curit\u00e9 => out=-1 + purge\\n\/\/ =====================================================\\nif (!inSafety) {\\n  flow.set('hdebut', null);\\n  flow.set('hfin', END_FIXED_MIN);\\n\\n  const payloadOut = {\\n    status_cp1: -1,\\n    soc_pct: Number(soc.toFixed(1)),\\n    cible_soc_pct: Number(target.toFixed(0)),\\n    need_pct: Number(need_pct.toFixed(1)),\\n    now_hhmm,\\n    inSafety: false,\\n    nowInWin: false,\\n    hdebut_min: null,\\n    hfin_min: normMin(END_FIXED_MIN),\\n    hdebut_hhmm: null,\\n    hfin_hhmm: hhmm(END_FIXED_MIN),\\n    l_status_cp1: l_status,\\n    reason: \\\"Hors fenetre de tir\\\"\\n  };\\n\\n  msg.payload = payloadOut;\\n\\n  node.status({\\n    fill: \\\"blue\\\",\\n    shape: \\\"dot\\\",\\n    text: `H_DYN SAFE-OFF | hors ${hhmm(START_FLOOR_MIN)}\u2192${hhmm(END_FIXED_MIN)} | out=-1`\\n  });\\n\\n  const logOut =\\n    `H_DYN @ ${now_hhmm}` +\\n    ` | SoC=${payloadOut.soc_pct}% cible=${payloadOut.cible_soc_pct}% (need=${payloadOut.need_pct}%)` +\\n    ` | safety=FALSE | out=-1 | l_status=${l_status}` +\\n    ` | reason=${payloadOut.reason}`;\\n\\n  return &#091;msg, { payload: logOut }];\\n}\\n\\n\/\/ =====================================================\\n\/\/ 1) Dans START_FLOOR\u2192END_FIXED : GEL si CP1 r\u00e9ellement ON (7 ou -7)\\n\/\/    => on NE RECALCULE PAS\\n\/\/    => la sortie reste \u00e0 7 tant qu'on est dans la fen\u00eatre m\u00e9moris\u00e9e\\n\/\/    => d\u00e8s qu'on sort de la fen\u00eatre m\u00e9moris\u00e9e => out=-1 + purge\\n\/\/ =====================================================\\nif (cp1ReallyOn) {\\n  const nowInWin_mem = (hdeb_mem !== null) ? isInWindow(nowMin, hdeb_mem, hfin_mem) : false;\\n\\n  \/\/ Fen\u00eatre prioritaire :\\n  \/\/ - dans la fen\u00eatre m\u00e9moris\u00e9e => 7\\n  \/\/ - hors fen\u00eatre m\u00e9moris\u00e9e => -1 (et purge)\\n  const out = nowInWin_mem ? 7 : -1;\\n  const reason = nowInWin_mem ? \\\"gel_keep_on\\\" : \\\"gel_outwin\\\";\\n\\n  if (!nowInWin_mem) {\\n    flow.set('hdebut', null);\\n    flow.set('hfin', END_FIXED_MIN);\\n  }\\n\\n  const payloadGel = {\\n    status_cp1: out,\\n    soc_pct: Number(soc.toFixed(1)),\\n    cible_soc_pct: Number(target.toFixed(0)),\\n    delta_soc_pct: Number((target - soc).toFixed(1)),\\n    now_hhmm,\\n    inSafetyNight: true,\\n    nowInWin: nowInWin_mem,\\n    hdebut_min: (hdeb_mem !== null ? normMin(hdeb_mem) : null),\\n    hfin_min: normMin(hfin_mem),\\n    hdebut_hhmm: (hdeb_mem !== null ? hhmm(hdeb_mem) : null),\\n    hfin_hhmm: hhmm(hfin_mem),\\n    l_status_cp1: l_status,\\n    reason\\n  };\\n\\n  msg.payload = payloadGel;\\n\\n  node.status({\\n    fill: (out === 7) ? \\\"green\\\" : \\\"blue\\\",\\n    shape: \\\"dot\\\",\\n    text: `H_DYN GEL | win=${payloadGel.hdebut_hhmm || \\\"--:--\\\"}\u2192${payloadGel.hfin_hhmm} | nowInWin=${payloadGel.nowInWin} | out=${out}`\\n  });\\n\\n  const logGel =\\n    `H_DYN @ ${now_hhmm}` +\\n    ` | SoC=${payloadGel.soc_pct}% cible=${payloadGel.cible_soc_pct}% (\u0394=${payloadGel.delta_soc_pct}%)` +\\n    ` | safety=TRUE` +\\n    ` | win=${payloadGel.hdebut_hhmm || \\\"--:--\\\"}\u2192${payloadGel.hfin_hhmm}` +\\n    ` | nowInWin=${payloadGel.nowInWin}` +\\n    ` | l_status=${l_status}` +\\n    ` | out=${out}` +\\n    ` | reason=${reason}`;\\n\\n  return &#091;msg, { payload: logGel }];\\n}\\n\\n\\n\/\/ =====================================================\\n\/\/ 2) DANS fen\u00eatre s\u00e9curit\u00e9, CP1 pas ON : si pas besoin => OFF + purge\\n\/\/ =====================================================\\nif (soc_ge_cible || inDeadband) {\\n  flow.set('hdebut', null);\\n  flow.set('hfin', END_FIXED_MIN);\\n\\n  const payloadOff = {\\n    status_cp1: -1,\\n    soc_pct: Number(soc.toFixed(1)),\\n    cible_soc_pct: Number(target.toFixed(0)),\\n    need_pct: Number(need_pct.toFixed(1)),\\n    now_hhmm,\\n    inSafety: true,\\n    nowInWin: false,\\n    hdebut_min: null,\\n    hfin_min: normMin(END_FIXED_MIN),\\n    hdebut_hhmm: null,\\n    hfin_hhmm: hhmm(END_FIXED_MIN),\\n    l_status_cp1: l_status,\\n    reason: soc_ge_cible ? \\\"soc>=cible\\\" : \\\"deadband\\\"\\n  };\\n\\n  msg.payload = payloadOff;\\n\\n  node.status({\\n    fill: \\\"blue\\\",\\n    shape: \\\"dot\\\",\\n    text: `H_DYN OFF | need=0 | out=-1`\\n  });\\n\\n  const logOff =\\n    `H_DYN @ ${now_hhmm}` +\\n    ` | SoC=${payloadOff.soc_pct}% cible=${payloadOff.cible_soc_pct}% (need=${payloadOff.need_pct}%)` +\\n    ` | safety=TRUE | out=-1 | l_status=${l_status}` +\\n    ` | reason=${payloadOff.reason}`;\\n\\n  return &#091;msg, { payload: logOff }];\\n}\\n\\n\/\/ =====================================================\\n\/\/ 3) DANS fen\u00eatre s\u00e9curit\u00e9, besoin>0 : calcul hdebut (hfin fixe END_FIXED)\\n\/\/ =====================================================\\n\\n\/\/ --- Calcul besoin (toujours positif) ---\\nconst ah_needed = (need_pct \/ 100) * CAP_AH;\\nconst bulk_min  = (I_AVG > 0) ? (ah_needed \/ I_AVG) * 60 : 0;\\nconst abs_min   = (target >= 95) ? ABS_MIN : 0;\\n\\nlet need_min = Math.ceil(bulk_min + abs_min);\\nneed_min = clamp(need_min, MIN_WIN, MAX_WIN);\\n\\n\/\/ D\u00e9but = fin - besoin, arrondi, wrap\\nlet start_min_raw = END_FIXED_MIN - need_min;\\nlet start_min = Math.floor(start_min_raw \/ STEP) * STEP;\\nlet wrapped = false;\\nif (start_min &lt; 0) { start_min += 1440; wrapped = true; }\\n\\n\/\/ Clamp au plancher 22:00 si start n\u2019est pas dans la fen\u00eatre s\u00e9curit\u00e9\\nlet clampedToFloor = false;\\nif (!isInSafety(start_min)) { start_min = START_FLOOR_MIN; clampedToFloor = true; }\\n\\n\/\/ M\u00e9morise la fen\u00eatre\\nflow.set('hdebut', start_min);\\nflow.set('hfin', END_FIXED_MIN);\\n\\n\/\/ nowInWin\\nconst nowInWin = isInWindow(nowMin, start_min, END_FIXED_MIN);\\n\\n\/\/ Fen\u00eatre prioritaire : dehors => -1\\nconst out = nowInWin ? 7 : -1;\\nconst reason = nowInWin ? \\\"need_inwin\\\" : \\\"need_outwin\\\";\\n\\n\/\/ Payload\\nconst payload = {\\n  status_cp1: out,\\n  soc_pct: Number(soc.toFixed(1)),\\n  cible_soc_pct: Number(target.toFixed(0)),\\n  need_pct: Number(need_pct.toFixed(1)),\\n  now_hhmm,\\n  inSafety: true,\\n  nowInWin,\\n  need_min: need_min,\\n  bulk_min: Math.ceil(bulk_min),\\n  abs_min: abs_min,\\n  hdebut_min: normMin(start_min),\\n  hfin_min: normMin(END_FIXED_MIN),\\n  hdebut_hhmm: hhmm(start_min),\\n  hfin_hhmm: hhmm(END_FIXED_MIN),\\n  l_status_cp1: l_status,\\n  reason\\n};\\n\\nmsg.payload = payload;\\n\\n\/\/ Status\\nnode.status({\\n  fill: (out === 7) ? \\\"green\\\" : \\\"blue\\\",\\n  shape: \\\"dot\\\",\\n  text: `H_DYN ${payload.hdebut_hhmm}\u2192${payload.hfin_hhmm} | nowInWin=${nowInWin} | out=${out}`\\n});\\n\\n\/\/ Log\\nconst flags = &#091;\\n  wrapped ? \\\"wrap\\\" : null,\\n  clampedToFloor ? \\\"clampFloor\\\" : null\\n].filter(Boolean).join(\\\",\\\");\\n\\nconst logTxt =\\n  `H_DYN @ ${now_hhmm}` +\\n  ` | SoC=${payload.soc_pct}% cible=${payload.cible_soc_pct}% (need=${payload.need_pct}%)` +\\n  ` | safety=TRUE` +\\n  ` | win=${payload.hdebut_hhmm}\u2192${payload.hfin_hhmm}` +\\n  ` | nowInWin=${nowInWin}` +\\n  ` | need=${need_min}m (bulk=${payload.bulk_min}m abs=${payload.abs_min}m)` +\\n  ` | l_status=${l_status}` +\\n  ` | out=${out}` +\\n  ` | reason=${reason}` +\\n  (flags ? ` | flags=${flags}` : \\\"\\\");\\n\\nreturn &#091;msg, { payload: logTxt }];\\n\",\n        \"outputs\": 2,\n        \"timeout\": 0,\n        \"noerr\": 0,\n        \"initialize\": \"\",\n        \"finalize\": \"\",\n        \"libs\": &#091;],\n        \"x\": 410,\n        \"y\": 1120,\n        \"wires\": &#091;\n            &#091;\n                \"0711c68c660d9c2f\",\n                \"e23bf7ba06f7abf5\"\n            ],\n            &#091;\n                \"c8d75201e67fa205\",\n                \"701744df73f35cf2\",\n                \"071b658e8fc627e4\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"56c0dd1a9c9f59d5\",\n        \"type\": \"comment\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"27c9a9a63c937a72\",\n        \"name\": \"Log Vers MQTT pour Stockage ds fichier via Home Assistant\",\n        \"info\": \"\",\n        \"x\": 460,\n        \"y\": 1620,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"3771889792a45c9c\",\n        \"type\": \"mqtt out\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"c64c5ecbe0654837\",\n        \"name\": \"Previ Conso J+1\",\n        \"topic\": \"mp2\/dess_remy\/previ_conso_6-22-J1\",\n        \"qos\": \"\",\n        \"retain\": \"\",\n        \"respTopic\": \"\",\n        \"contentType\": \"\",\n        \"userProps\": \"\",\n        \"correl\": \"\",\n        \"expiry\": \"\",\n        \"broker\": \"502248144035edbf\",\n        \"x\": 680,\n        \"y\": 320,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"58133dfe4f341acc\",\n        \"type\": \"comment\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"f8afda3620427140\",\n        \"name\": \"Calcul Heure de d\u00e9but activation de la Charge Program\u00e9e 1 de ESS Victron\",\n        \"info\": \"\",\n        \"x\": 580,\n        \"y\": 1040,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"0711c68c660d9c2f\",\n        \"type\": \"debug\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"f8afda3620427140\",\n        \"name\": \"debug 108\",\n        \"active\": true,\n        \"tosidebar\": true,\n        \"console\": false,\n        \"tostatus\": true,\n        \"complete\": \"true\",\n        \"targetType\": \"full\",\n        \"statusVal\": \"payload\",\n        \"statusType\": \"auto\",\n        \"x\": 910,\n        \"y\": 1080,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"c8d75201e67fa205\",\n        \"type\": \"debug\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"f8afda3620427140\",\n        \"name\": \"debug 109\",\n        \"active\": true,\n        \"tosidebar\": true,\n        \"console\": false,\n        \"tostatus\": true,\n        \"complete\": \"true\",\n        \"targetType\": \"full\",\n        \"statusVal\": \"payload\",\n        \"statusType\": \"auto\",\n        \"x\": 910,\n        \"y\": 1140,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"f26bc3295655d978\",\n        \"type\": \"ui_numeric\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"c64c5ecbe0654837\",\n        \"name\": \"\",\n        \"label\": \"numeric\",\n        \"tooltip\": \"\",\n        \"group\": \"c8a7d12bb7f76f27\",\n        \"order\": 17,\n        \"width\": 0,\n        \"height\": 0,\n        \"wrap\": false,\n        \"passthru\": true,\n        \"topic\": \"topic\",\n        \"topicType\": \"msg\",\n        \"format\": \"{{value}}\",\n        \"min\": 0,\n        \"max\": \"100\",\n        \"step\": 1,\n        \"className\": \"\",\n        \"x\": 640,\n        \"y\": 380,\n        \"wires\": &#091;\n            &#091;\n                \"100b50b7dd6436f3\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"100b50b7dd6436f3\",\n        \"type\": \"change\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"d\": true,\n        \"g\": \"c64c5ecbe0654837\",\n        \"name\": \"\",\n        \"rules\": &#091;\n            {\n                \"t\": \"set\",\n                \"p\": \"dess_soc_cible_pct\",\n                \"pt\": \"flow\",\n                \"to\": \"payload\",\n                \"tot\": \"msg\"\n            }\n        ],\n        \"action\": \"\",\n        \"property\": \"\",\n        \"from\": \"\",\n        \"to\": \"\",\n        \"reg\": false,\n        \"x\": 910,\n        \"y\": 380,\n        \"wires\": &#091;\n            &#091;]\n        ]\n    },\n    {\n        \"id\": \"af7be0afa7182573\",\n        \"type\": \"function\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"name\": \"Change\",\n        \"func\": \"\/\/ Equivalent d'un Change : set flow.valid_cp_ess = msg.payload\\nflow.set(\\\"valid_cp_ess\\\", msg.payload);\\n\\nnode.status({\\n  fill: \\\"green\\\",\\n  shape: \\\"dot\\\",\\n  text: `valid_cp_ess=${String(msg.payload)}`\\n});\\n\\nreturn msg;\\n\",\n        \"outputs\": 1,\n        \"timeout\": 0,\n        \"noerr\": 0,\n        \"initialize\": \"\",\n        \"finalize\": \"\",\n        \"libs\": &#091;],\n        \"x\": 460,\n        \"y\": 540,\n        \"wires\": &#091;\n            &#091;]\n        ]\n    },\n    {\n        \"id\": \"b79542847a8290f1\",\n        \"type\": \"function\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"c64c5ecbe0654837\",\n        \"name\": \"Change\",\n        \"func\": \"\/\/ Equivalent d'un Change : set flow.niveauforcp1 = msg.payload\\nflow.set(\\\"niveauforcp1\\\", msg.payload);\\n\\nnode.status({\\n  fill: \\\"green\\\",\\n  shape: \\\"dot\\\",\\n  text: `niveauforcp1=${String(msg.payload)}`\\n});\\n\\nreturn msg;\\n\",\n        \"outputs\": 1,\n        \"timeout\": 0,\n        \"noerr\": 0,\n        \"initialize\": \"\",\n        \"finalize\": \"\",\n        \"libs\": &#091;],\n        \"x\": 460,\n        \"y\": 280,\n        \"wires\": &#091;\n            &#091;\n                \"30724406b2c3e9a6\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"e8f6758392b2f7d7\",\n        \"type\": \"function\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"c64c5ecbe0654837\",\n        \"name\": \"Change\",\n        \"func\": \"\/\/ Equivalent d'un Change : set flow.forc100 = msg.payload\\nflow.set(\\\"forc100\\\", msg.payload);\\n\\nnode.status({\\n  fill: \\\"green\\\",\\n  shape: \\\"dot\\\",\\n  text: `forc100=${String(msg.payload)}`\\n});\\n\\nreturn msg;\\n\",\n        \"outputs\": 1,\n        \"timeout\": 0,\n        \"noerr\": 0,\n        \"initialize\": \"\",\n        \"finalize\": \"\",\n        \"libs\": &#091;],\n        \"x\": 460,\n        \"y\": 340,\n        \"wires\": &#091;\n            &#091;\n                \"30724406b2c3e9a6\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"5b9f7733139cceb7\",\n        \"type\": \"victron-input-ess\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"service\": \"com.victronenergy.settings\",\n        \"path\": \"\/Settings\/CGwacs\/BatteryLife\/Schedule\/Charge\/0\/Day\",\n        \"serviceObj\": {\n            \"service\": \"com.victronenergy.settings\",\n            \"name\": \"Venus settings\"\n        },\n        \"pathObj\": {\n            \"path\": \"\/Settings\/CGwacs\/BatteryLife\/Schedule\/Charge\/0\/Day\",\n            \"type\": \"enum\",\n            \"name\": \"Schedule 1: Day\",\n            \"enum\": {\n                \"0\": \"Sunday\",\n                \"1\": \"Monday\",\n                \"2\": \"Tuesday\",\n                \"3\": \"Wednesday\",\n                \"4\": \"Thursday\",\n                \"5\": \"Friday\",\n                \"6\": \"Saturday\",\n                \"7\": \"Every day\",\n                \"8\": \"Weekdays\",\n                \"9\": \"Weekends\",\n                \"11\": \"Monthly\",\n                \"-1\": \"Disabled\"\n            },\n            \"remarks\": \"&lt;p>A negative value means that the schedule has been de-activated.&lt;\/p>\",\n            \"mode\": \"both\"\n        },\n        \"name\": \"Status CP1\",\n        \"onlyChanges\": false,\n        \"x\": 650,\n        \"y\": 540,\n        \"wires\": &#091;\n            &#091;\n                \"7987ad4db47bf444\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"51dc07974be1a13d\",\n        \"type\": \"function\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"name\": \"Change\",\n        \"func\": \"\/\/ Equivalent d'un Change :\\nflow.set(\\\"l_status_cp1\\\", msg.payload);\\n\\nnode.status({\\n  fill: \\\"green\\\",\\n  shape: \\\"dot\\\",\\n  text: `l_status_cp1=${String(msg.payload)}`\\n});\\n\\nreturn msg;\\n\",\n        \"outputs\": 1,\n        \"timeout\": 0,\n        \"noerr\": 0,\n        \"initialize\": \"\",\n        \"finalize\": \"\",\n        \"libs\": &#091;],\n        \"x\": 980,\n        \"y\": 540,\n        \"wires\": &#091;\n            &#091;]\n        ]\n    },\n    {\n        \"id\": \"7987ad4db47bf444\",\n        \"type\": \"delay\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"name\": \"\",\n        \"pauseType\": \"delay\",\n        \"timeout\": \"5\",\n        \"timeoutUnits\": \"seconds\",\n        \"rate\": \"1\",\n        \"nbRateUnits\": \"1\",\n        \"rateUnits\": \"second\",\n        \"randomFirst\": \"1\",\n        \"randomLast\": \"5\",\n        \"randomUnits\": \"seconds\",\n        \"drop\": false,\n        \"allowrate\": false,\n        \"outputs\": 1,\n        \"x\": 820,\n        \"y\": 540,\n        \"wires\": &#091;\n            &#091;\n                \"51dc07974be1a13d\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"a5d3d6de6b719c0a\",\n        \"type\": \"link out\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"name\": \"link out 9\",\n        \"mode\": \"link\",\n        \"links\": &#091;\n            \"573c180d02c5fc31\"\n        ],\n        \"x\": 295,\n        \"y\": 800,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"c84e616b7483e3a7\",\n        \"type\": \"rbe\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"name\": \"\",\n        \"func\": \"rbe\",\n        \"gap\": \"\",\n        \"start\": \"\",\n        \"inout\": \"out\",\n        \"septopics\": true,\n        \"property\": \"payload\",\n        \"topi\": \"topic\",\n        \"x\": 310,\n        \"y\": 540,\n        \"wires\": &#091;\n            &#091;\n                \"af7be0afa7182573\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"0980d6b80b2d707a\",\n        \"type\": \"rbe\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"c64c5ecbe0654837\",\n        \"name\": \"\",\n        \"func\": \"rbe\",\n        \"gap\": \"\",\n        \"start\": \"\",\n        \"inout\": \"out\",\n        \"septopics\": true,\n        \"property\": \"payload\",\n        \"topi\": \"topic\",\n        \"x\": 310,\n        \"y\": 280,\n        \"wires\": &#091;\n            &#091;\n                \"b79542847a8290f1\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"6c9e2d97af61d80e\",\n        \"type\": \"rbe\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"c64c5ecbe0654837\",\n        \"name\": \"\",\n        \"func\": \"rbe\",\n        \"gap\": \"\",\n        \"start\": \"\",\n        \"inout\": \"out\",\n        \"septopics\": true,\n        \"property\": \"payload\",\n        \"topi\": \"topic\",\n        \"x\": 310,\n        \"y\": 340,\n        \"wires\": &#091;\n            &#091;\n                \"e8f6758392b2f7d7\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"0b9a873b13652106\",\n        \"type\": \"inject\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"name\": \"-1\",\n        \"props\": &#091;\n            {\n                \"p\": \"payload\"\n            },\n            {\n                \"p\": \"topic\",\n                \"vt\": \"str\"\n            }\n        ],\n        \"repeat\": \"\",\n        \"crontab\": \"\",\n        \"once\": false,\n        \"onceDelay\": 0.1,\n        \"topic\": \"\",\n        \"payload\": \"-1\",\n        \"payloadType\": \"num\",\n        \"x\": 330,\n        \"y\": 600,\n        \"wires\": &#091;\n            &#091;\n                \"cbe71d4c64597512\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"19d7eb29a5c60cdc\",\n        \"type\": \"inject\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"c64c5ecbe0654837\",\n        \"name\": \"21:45\",\n        \"props\": &#091;\n            {\n                \"p\": \"payload\"\n            },\n            {\n                \"p\": \"topic\",\n                \"vt\": \"str\"\n            }\n        ],\n        \"repeat\": \"\",\n        \"crontab\": \"45 21 * * *\",\n        \"once\": false,\n        \"onceDelay\": \"5\",\n        \"topic\": \"\",\n        \"payload\": \"\",3)\n        \"payloadType\": \"date\",\n        \"x\": 140,\n        \"y\": 160,\n        \"wires\": &#091;\n            &#091;\n                \"4f987019c9af7859\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"d9004c05cabbfc69\",\n        \"type\": \"mqtt out\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"c64c5ecbe0654837\",\n        \"name\": \"Previ Prod J+1\",\n        \"topic\": \"mp2\/dess_remy\/previ_prod_6-22-J1\",\n        \"qos\": \"\",\n        \"retain\": \"\",\n        \"respTopic\": \"\",\n        \"contentType\": \"\",\n        \"userProps\": \"\",\n        \"correl\": \"\",\n        \"expiry\": \"\",\n        \"broker\": \"502248144035edbf\",\n        \"x\": 960,\n        \"y\": 160,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"03c36694b589e110\",\n        \"type\": \"victron-output-settings\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"51e27c869bb5e6f1\",\n        \"service\": \"com.victronenergy.settings\",\n        \"path\": \"\/Settings\/CGwacs\/BatteryLife\/State\",\n        \"serviceObj\": {\n            \"service\": \"com.victronenergy.settings\",\n            \"name\": \"Venus settings\"\n        },\n        \"pathObj\": {\n            \"path\": \"\/Settings\/CGwacs\/BatteryLife\/State\",\n            \"type\": \"enum\",\n            \"name\": \"ESS BatteryLife state\",\n            \"enum\": {\n                \"0\": \"Unused, BL disabled\",\n                \"1\": \"Restarting\",\n                \"2\": \"Self-consumption\",\n                \"3\": \"Self-consumption\",\n                \"4\": \"Self-consumption\",\n                \"5\": \"Discharge disabled\",\n                \"6\": \"Force charge\",\n                \"7\": \"Sustain\",\n                \"8\": \"Low Soc Recharge\",\n                \"9\": \"Keep batteries charged\",\n                \"10\": \"BL Disabled\",\n                \"11\": \"BL Disabled (Low SoC)\",\n                \"12\": \"BL Disabled (Low SOC recharge)\"\n            }\n        },\n        \"name\": \"\",\n        \"onlyChanges\": false,\n        \"x\": 830,\n        \"y\": 1380,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"ab48449b78d44baf\",\n        \"type\": \"victron-input-settings\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"51e27c869bb5e6f1\",\n        \"service\": \"com.victronenergy.settings\",\n        \"path\": \"\/Settings\/CGwacs\/BatteryLife\/State\",\n        \"serviceObj\": {\n            \"service\": \"com.victronenergy.settings\",\n            \"name\": \"Venus settings\"\n        },\n        \"pathObj\": {\n            \"path\": \"\/Settings\/CGwacs\/BatteryLife\/State\",\n            \"type\": \"enum\",\n            \"name\": \"ESS BatteryLife state\",\n            \"enum\": {\n                \"0\": \"Unused, BL disabled\",\n                \"1\": \"Restarting\",\n                \"2\": \"Self-consumption\",\n                \"3\": \"Self-consumption\",\n                \"4\": \"Self-consumption\",\n                \"5\": \"Discharge disabled\",\n                \"6\": \"Force charge\",\n                \"7\": \"Sustain\",\n                \"8\": \"Low Soc Recharge\",\n                \"9\": \"Keep batteries charged\",\n                \"10\": \"BL Disabled\",\n                \"11\": \"BL Disabled (Low SoC)\",\n                \"12\": \"BL Disabled (Low SOC recharge)\"\n            }\n        },\n        \"name\": \"\",\n        \"onlyChanges\": false,\n        \"x\": 830,\n        \"y\": 1440,\n        \"wires\": &#091;\n            &#091;]\n        ]\n    },\n    {\n        \"id\": \"1f1cd907d3eec9a0\",\n        \"type\": \"function\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"51e27c869bb5e6f1\",\n        \"name\": \"Surveillance U Batt V1\",\n        \"func\": \"\/\/ =====================================================\\n\/\/ GUARD Ubatt -> ESS BatteryLife State\\n\/\/ Objectif : ne forcer QUE l'\u00e9tat 5 (Discharge disabled) si Ubatt trop basse,\\n\/\/            sinon rel\u00e2cher et laisser BatteryLife d\u00e9cider (NE RIEN \u00e9crire).\\n\/\/ Dbus path: \/Settings\/CGwacs\/BatteryLife\/State\\n\/\/ Sorties :\\n\/\/ 1) msgWrite (seulement quand il faut \u00e9crire)\\n\/\/ 2) log texte (fichier HA)\\n\/\/ =====================================================\\n\\nconst V_CUTOFF  = 46.0; \/\/ entree protection\\nconst V_RELEASE = 46.8; \/\/ sortie protection (hyst\u00e9r\u00e9sis)\\n\\n\/\/ lecture ubatt (adapte si besoin)\\nlet ubatt = Number(flow.get(\\\"ubatt\\\"));\\nif (!Number.isFinite(ubatt)) ubatt = Number(msg.payload);\\nif (!Number.isFinite(ubatt)) {\\n  node.status({ fill:\\\"red\\\", shape:\\\"ring\\\", text:\\\"Ubatt absente\\\" });\\n  return &#091;null, { payload: \\\"BL_GUARD | ERREUR ubatt absente\\\" }];\\n}\\n\\n\/\/ \u00e9tat interne\\nlet forced = (flow.get(\\\"bl_forced5\\\") === true);\\n\\n\/\/ d\u00e9cision hysteresis\\nif (!forced &amp;&amp; ubatt &lt; V_CUTOFF) forced = true;\\nelse if (forced &amp;&amp; ubatt > V_RELEASE) forced = false;\\n\\n\/\/ d\u00e9tecter changement d'\u00e9tat du mode \\\"for\u00e7age\\\"\\nconst prevForced = (flow.get(\\\"bl_forced5_prev\\\") === true);\\nflow.set(\\\"bl_forced5\\\", forced);\\nflow.set(\\\"bl_forced5_prev\\\", forced);\\n\\n\/\/ timestamp\\nconst now = new Date();\\nconst hhmm = `${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}`;\\n\\n\/\/ si on n'est pas en protection => NE RIEN \u00e9crire (laisser BL d\u00e9cider)\\nif (!forced) {\\n  node.status({\\n    fill: \\\"green\\\",\\n    shape: \\\"dot\\\",\\n    text: `BL libre | Ubatt=${ubatt.toFixed(2)}V`\\n  });\\n\\n  \/\/ log seulement quand on sort du mode forc\u00e9 (\u00e9v\u00e9nement significatif)\\n  if (prevForced) {\\n    const log = `BL_GUARD @ ${hhmm} | Ubatt=${ubatt.toFixed(2)}V | RELEASE -> BatteryLife libre`;\\n    return &#091;null, { payload: log }];\\n  }\\n  return &#091;null, null];\\n}\\n\\n\/\/ forced == true => on veut imposer State=5\\n\/\/ anti-spam: n'\u00e9crire que si on vient d'entrer en mode forc\u00e9 ou si derni\u00e8re \u00e9criture != 5\\nconst lastWritten = Number(flow.get(\\\"bl_last_written\\\"));\\nconst needWrite = (!Number.isFinite(lastWritten) || lastWritten !== 5 || !prevForced);\\n\\nflow.set(\\\"bl_last_written\\\", 5);\\n\\nnode.status({\\n  fill: \\\"red\\\",\\n  shape: \\\"dot\\\",\\n  text: `Discharge DISABLED (5) | Ubatt=${ubatt.toFixed(2)}V`\\n});\\n\\n\/\/ message write\\nconst msgWrite = {\\n  payload: 5,\\n  \/\/ si ton node write utilise msg.topic:\\n  \/\/ topic: \\\"\/Settings\/CGwacs\/BatteryLife\/State\\\"\\n};\\n\\n\/\/ log\\nconst log = `BL_GUARD @ ${hhmm} | Ubatt=${ubatt.toFixed(2)}V | FORCE State=5 (discharge disabled)`;\\n\\nreturn &#091;needWrite ? msgWrite : null, { payload: log }];\\n\",\n        \"outputs\": 2,\n        \"timeout\": 0,\n        \"noerr\": 0,\n        \"initialize\": \"\",\n        \"finalize\": \"\",\n        \"libs\": &#091;],\n        \"x\": 420,\n        \"y\": 1420,\n        \"wires\": &#091;\n            &#091;\n                \"03c36694b589e110\"\n            ],\n            &#091;\n                \"f32446775a09a1fc\",\n                \"3cac79c890574a77\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"800f167bb831e02b\",\n        \"type\": \"victron-input-battery\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"51e27c869bb5e6f1\",\n        \"service\": \"com.victronenergy.battery\/277\",\n        \"path\": \"\/Dc\/0\/Voltage\",\n        \"serviceObj\": {\n            \"service\": \"com.victronenergy.battery\/277\",\n            \"name\": \"SmartShunt 500A\/50mV\"\n        },\n        \"pathObj\": {\n            \"path\": \"\/Dc\/0\/Voltage\",\n            \"type\": \"float\",\n            \"name\": \"Battery voltage (V)\"\n        },\n        \"name\": \"U Batteries\",\n        \"onlyChanges\": false,\n        \"roundValues\": \"2\",\n        \"x\": 120,\n        \"y\": 1420,\n        \"wires\": &#091;\n            &#091;\n                \"b75a9e08a5dbf223\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"b75a9e08a5dbf223\",\n        \"type\": \"change\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"51e27c869bb5e6f1\",\n        \"name\": \"UB\",\n        \"rules\": &#091;\n            {\n                \"t\": \"set\",\n                \"p\": \"ubatt\",\n                \"pt\": \"flow\",\n                \"to\": \"payload\",\n                \"tot\": \"msg\"\n            }\n        ],\n        \"action\": \"\",\n        \"property\": \"\",\n        \"from\": \"\",\n        \"to\": \"\",\n        \"reg\": false,\n        \"x\": 250,\n        \"y\": 1420,\n        \"wires\": &#091;\n            &#091;\n                \"1f1cd907d3eec9a0\"\n            ]\n        ],\n        \"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\"\n    },\n    {\n        \"id\": \"f32446775a09a1fc\",\n        \"type\": \"link out\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"51e27c869bb5e6f1\",\n        \"name\": \"link out 10\",\n        \"mode\": \"link\",\n        \"links\": &#091;\n            \"573c180d02c5fc31\"\n        ],\n        \"x\": 605,\n        \"y\": 1460,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"d61899b9b703dfe6\",\n        \"type\": \"comment\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"51e27c869bb5e6f1\",\n        \"name\": \"Surveillance Tension Batteries: 46.0&lt;->46.8V\",\n        \"info\": \"\",\n        \"x\": 510,\n        \"y\": 1340,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"a70c71330cb91fb6\",\n        \"type\": \"ui_text\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"27c9a9a63c937a72\",\n        \"group\": \"c8a7d12bb7f76f27\",\n        \"order\": 7,\n        \"width\": \"12\",\n        \"height\": \"2\",\n        \"name\": \"\",\n        \"label\": \"Log HA :\",\n        \"format\": \"{{msg.payload}}\",\n        \"layout\": \"row-spread\",\n        \"className\": \"\",\n        \"style\": false,\n        \"font\": \"\",\n        \"fontSize\": 16,\n        \"color\": \"#000000\",\n        \"x\": 700,\n        \"y\": 1660,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"66037dc3f0b544c2\",\n        \"type\": \"inject\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"51e27c869bb5e6f1\",\n        \"name\": \"\",\n        \"props\": &#091;\n            {\n                \"p\": \"payload\"\n            },\n            {\n                \"p\": \"topic\",\n                \"vt\": \"str\"\n            }\n        ],\n        \"repeat\": \"\",\n        \"crontab\": \"\",\n        \"once\": false,\n        \"onceDelay\": 0.1,\n        \"topic\": \"\",\n        \"payload\": \"\",\n        \"payloadType\": \"date\",\n        \"x\": 370,\n        \"y\": 1380,\n        \"wires\": &#091;\n            &#091;\n                \"03c36694b589e110\"\n            ]\n        ]\n    },\n    {\n        \"id\": \"3cac79c890574a77\",\n        \"type\": \"ui_text\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"51e27c869bb5e6f1\",\n        \"group\": \"c8a7d12bb7f76f27\",\n        \"order\": 2,\n        \"width\": \"12\",\n        \"height\": \"2\",\n        \"name\": \"\",\n        \"label\": \"Log Surv Bat:\",\n        \"format\": \"{{msg.payload}}\",\n        \"layout\": \"row-spread\",\n        \"className\": \"\",\n        \"style\": false,\n        \"font\": \"\",\n        \"fontSize\": 16,\n        \"color\": \"#000000\",\n        \"x\": 720,\n        \"y\": 1500,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"7914d10c7560953d\",\n        \"type\": \"debug\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"c64c5ecbe0654837\",\n        \"name\": \"debug 119\",\n        \"active\": true,\n        \"tosidebar\": true,\n        \"console\": false,\n        \"tostatus\": true,\n        \"complete\": \"true\",\n        \"targetType\": \"full\",\n        \"statusVal\": \"payload\",\n        \"statusType\": \"auto\",\n        \"x\": 970,\n        \"y\": 320,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"7f9d7f52a663fbbc\",\n        \"type\": \"link out\",\n        \"z\": \"5fe7408840b1cdf3\",\n        \"g\": \"9669903f47819092\",\n        \"name\": \"link out Graph\",\n        \"mode\": \"link\",\n        \"links\": &#091;\n            \"bbbf1b3d884bf814\"\n        ],\n        \"x\": 945,\n        \"y\": 900,\n        \"wires\": &#091;]\n    },\n    {\n        \"id\": \"502248144035edbf\",\n        \"type\": \"mqtt-broker\",\n        \"name\": \"MQTTHA\",\n        \"broker\": \"192.168.0.37\",\n        \"port\": \"1883\",\n        \"clientid\": \"\",\n        \"autoConnect\": true,\n        \"usetls\": false,\n        \"protocolVersion\": \"4\",\n        \"keepalive\": \"60\",\n        \"cleansession\": true,\n        \"autoUnsubscribe\": true,\n        \"birthTopic\": \"\",\n        \"birthQos\": \"0\",\n        \"birthPayload\": \"\",\n        \"birthMsg\": {},\n        \"closeTopic\": \"\",\n        \"closeQos\": \"0\",\n        \"closePayload\": \"\",\n        \"closeMsg\": {},\n        \"willTopic\": \"\",\n        \"willQos\": \"0\",\n        \"willPayload\": \"\",\n        \"willMsg\": {},\n        \"userProps\": \"\",\n        \"sessionExpiry\": \"\"\n    },\n    {\n        \"id\": \"c8a7d12bb7f76f27\",\n        \"type\": \"ui_group\",\n        \"name\": \"DESS\",\n        \"tab\": \"8c1c69b7bf39eb5b\",\n        \"order\": 1,\n        \"disp\": true,\n        \"width\": \"12\",\n        \"collapse\": false,\n        \"className\": \"\"\n    },\n    {\n        \"id\": \"5e3a89e5eaeb1cb4\",\n        \"type\": \"config-vrm-api\",\n        \"name\": \"vrm_remy\"\n    },\n    {\n        \"id\": \"8c1c69b7bf39eb5b\",\n        \"type\": \"ui_tab\",\n        \"name\": \"MP2\",\n        \"icon\": \"dashboard\",\n        \"disabled\": false,\n        \"hidden\": false\n    }\n]<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Intro Depuis plusieurs mois, mon installation photovolta\u00efque Victron a nettement \u00e9volu\u00e9 : nouveaux capteurs, nouveaux besoins (dont la gestion des jours EDF Tempo), consommation nocturne, s\u00e9curisation du SoC, et surtout &hellip; <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4,6],"tags":[59,66,63],"class_list":["post-4210","post","type-post","status-publish","format-standard","hentry","category-homeassistant","category-photovoltaique","tag-mqtt","tag-nodered","tag-victron"],"_links":{"self":[{"href":"https:\/\/domo.rem81.com\/index.php\/wp-json\/wp\/v2\/posts\/4210","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/domo.rem81.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/domo.rem81.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/domo.rem81.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/domo.rem81.com\/index.php\/wp-json\/wp\/v2\/comments?post=4210"}],"version-history":[{"count":32,"href":"https:\/\/domo.rem81.com\/index.php\/wp-json\/wp\/v2\/posts\/4210\/revisions"}],"predecessor-version":[{"id":4346,"href":"https:\/\/domo.rem81.com\/index.php\/wp-json\/wp\/v2\/posts\/4210\/revisions\/4346"}],"wp:attachment":[{"href":"https:\/\/domo.rem81.com\/index.php\/wp-json\/wp\/v2\/media?parent=4210"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/domo.rem81.com\/index.php\/wp-json\/wp\/v2\/categories?post=4210"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/domo.rem81.com\/index.php\/wp-json\/wp\/v2\/tags?post=4210"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}