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


Analyse du flow (version 2026)
A) Bloc “Prévisions VRM : production PV + consommation J+1 (6h → 22h)”
Le flow ne se limite plus à “additionner des totaux” : il travaille désormais à la journée suivante (J+1) et sur la plage utile 06:00 → 22:00, ce qui colle au besoin réel (tenir la journée hors HC).
1) Prévision de production PV J+1
- Un nœud VRM API récupère la série
solar_yield_forecast(format typique :records.solar_yield_forecast = [[timestamp_ms, Wh], ...]). - Une fonction
Extrac 6:22:- filtre les points correspondant au jour J+1
- somme uniquement les valeurs entre 6h et 22h
- convertit Wh → kWh (arrondi à 0,1 kWh)
- stocke en
flow.previ_prod_6_22(et optionnellementflow.previ_prod_total) - publie le résultat sur MQTT :
mp2/dess_remy/prod_total(QoS 2 + retain)
- Un change node
Save previ_prodmémorise aussi la valeur utilisée par la suite viaflow.previ_prod.
2) Prévision de consommation J+1
- Un second nœud VRM API récupère la prévision de consommation
vrm_consumption_fc(série horaire). - La fonction
Extraction 6-22h:- filtre J+1
- somme uniquement 06:00 → 22:00
- convertit Wh → kWh (1 décimale)
- injecte le résultat dans
flow.conso_j1
À ce stade, Node-RED dispose de deux entrées cohérentes pour dimensionner la batterie sur la journée suivante :
- Production PV attendue (6–22)
- Consommation attendue (6–22)
B) Bloc “Calcul SoC cible (logique DoD J+1)”
C’est la nouveauté structurante du flow.
La fonction Calcul Soc Cible calcule un SoC cible “métier” à partir :
flow.conso_j1(conso prévue 6–22 J+1)flow.previ_prod(prod prévue 6–22 J+1)flow.soc(SoC actuel)- paramètres (capacité batterie, rendement, soc_min, marge, part réseau acceptée, etc.)
Elle produit et stocke :
flow.dess_soc_cible_pct: SoC cible calculéflow.dess_dod_pct: DoD requis (indicatif)
Ce SoC cible calculé devient ensuite la référence prioritaire pour le pilotage CP1.
C) Bloc “Entrées Home Assistant (autorisations et forçages)”
Trois topics MQTT (HA → Node-RED) restent le moyen simple de reprendre la main :
ha/mp2/cp/validcp: autorisation globale de charge programmée (ON/OFF)
→ stocké englobal.valid_cp_essha/mp2/cp/forcage100: forçage manuel (ON/OFF)
→ stocké enflow.forc100ha/mp2/cp/niveauforcagecp1: SoC cible forcé (ex : 80)
→ stocké enflow.niveauforcp1
Ces entrées sont consommées directement par la fonction de pilotage CP1.
D) Bloc “Horloge CP 22h → 6h + horaires dynamiques”
Le pilotage temporel est maintenant plus propre :
- Un BigTimer définit la fenêtre 22:00 → 06:00 (sortie on/off).
- La fonction
H Dynamique V3:- prend la fenêtre BigTimer “comme vérité”
- calcule hdebut (minutes) en fonction du besoin (SoC actuel → SoC cible)
- écrit :
flow.horloge(0/1)flow.hdebut(minutes)flow.hfin(minutes, fin de fenêtre)
- sort un log clair vers un widget dashboard (“Log H Dyn”).
Ensuite, un recalcul “souple” du SoC est prévu :
SOC ts les 15mn: ne recalculera que toutes les 15 minutes et uniquement si BigTimer est OFF, pour éviter les oscillations pendant la fenêtre de charge.
E) Bloc “Pilotage CP1 V6 : SoC cible + commande Victron”
La fonction Pilotage CP1 V6 est le cœur du pilotage.
1) Calcul du seuil SoC cible (priorités)
- SoC cible calculé via DoD J+1 :
flow.dess_soc_cible_pct(si valide) - Sinon fallback historique :
seuil = 120 - 2.5 × prod - Enfin, si forçage HA actif (
forcage100 == on) :seuil = niveauforcp1
Puis :
- arrondi au pas de 5%
- bornage 0–100 (avec règle “<15% ⇒ 0” conservée)
2) Activation de la charge programmée
- si HA autorise ET horloge==1 → activation (payload 7, i.e. “Every day”)
- sinon → désactivation (payload -1)
3) Start / Duration
- conversion minutes → secondes pour alimenter :
/Schedule/Charge/0/Start/Schedule/Charge/0/Duration
4) Log synthétique
Un log unique et lisible (UI + debug) affichant :
SoC actuel, Seuil retenu, état forçage, durée HC (HH:MM), horloge, autorisation HA, et valeur effectivement envoyée.
F) Bloc “Écriture des settings dans Venus OS + MQTT sortant”
1) Écriture Venus OS (victron-output-ess)
- Validation CP :
/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Day - Heure début :
/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Start - Durée :
/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Duration - Cible SoC :
/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Soc - (option) Autoconsommation :
/Settings/CGwacs/BatteryLife/Schedule/Charge/0/AllowDischarge
2) MQTT sortant (supervision Home Assistant)
mp2/dess_remy/prod_total(prévision PV J+1 6–22, QoS2 retain)mp2/dess_remy/cible_socmp2/multiplus2/valide_cp(converti en on/off via mapping -1/7)mp2/dess_remy/h_debut(format HH:MM)mp2/dess_remy/duree(format HH:MM)
Détails techniques (extraits utiles mis à jour)
1) Extraction PV J+1 6–22 (principe)
- source :
records.solar_yield_forecast - filtre : jour J+1 + plage 06:00–22:00
- conversion Wh → kWh (1 décimale)
- stockage :
flow.previ_prod_6_22(+flow.previ_prodvia change node)
2) Extraction conso J+1 6–22 (principe)
- source :
records.vrm_consumption_fc - filtre : jour J+1 + plage 06:00–22:00
- conversion Wh → kWh (1 décimale)
- stockage :
flow.conso_j1
3) Calcul SoC cible + activation CP (principe mis à jour)
- SoC cible prioritaire :
flow.dess_soc_cible_pct(DoD J+1) - fallback :
120 - 2.5 × prod - forçage HA : override total
- activation CP : uniquement si
global.valid_cp_essautorise ETflow.horloge==1
Dashboard Node red:

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

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

Côté HA, tu peux :
- créer des
sensorMQTT pourprod_total,cible_soc,duree, etc. - utiliser
input_boolean/input_numberpourvalidcp,forcage100,niveauforcagecp1, et publier vers les topicsha/mp2/cp/....
Fichier victron.yaml permettant de récuperer les mqtt:
mqtt:
sensor:
# MQTT
- name: "MP2 SOC theorique par MQTT"
unique_id: mp2_soc_theorique_par_mqtt
state_topic: "mp2/batteries/soc_theorique"
unit_of_measurement: '%'
device_class: battery
state_class: measurement
- name: "MP2 VALIDE CP MQTT"
unique_id: mp2_valide_cp_par_mqtt
state_topic: "mp2/multiplus2/valide_cp"
- name: "Capacité Batterie Estimée par NR"
state_topic: "mp2/batteries/capacity_est_ah"
unit_of_measurement: "Ah"
icon: mdi:battery-charging-outline
state_class: measurement
# DESS Remy
- name: "MP2 DESS Cible SOC par MQTT"
unique_id: mp2_dess_cible_soc
state_topic: "mp2/dess_remy/cible_soc"
unit_of_measurement: '%'
device_class: battery
state_class: measurement
- name: "MP2 DESS Difference Cible-SOC"
unique_id: mp2_difference_cible_soc
state_topic: "mp2/dess_remy/diff_soc"
unit_of_measurement: '%'
device_class: battery
state_class: measurement
- name: "MP2 DESS H_Debut DESS Remy"
unique_id: mp2_dess_h_debut
state_topic: "mp2/dess_remy/h_debut"
- name: "MP2 DESS Durée DESS Remy"
unique_id: mp2_dess_duree
state_topic: "mp2/dess_remy/duree"
- name: "MP2 DESS Remy Log Audit"
unique_id: mp2_dess_log_audit
state_topic: "mp2/dess_remy/log_audit"
- name: "MP2 DESS Previ Conso 6-22 J1"
unique_id: mp2_dess_previ_conso_6_22_j1
state_topic: "mp2/dess_remy/previ_conso_6-22-J1"
unit_of_measurement: "kWh"
device_class: "energy"
state_class: "measurement"
# Victron
#modbus:
# - name: cerbo
# host: 192.168.0.86
# type: tcp
# port: 502
# switches:
# - name: "MP2 cde"
# slave: 227
# address: 33
# command_on: 3
# command_off: 4
# verify:
# input_type: holding
# address: 33
# state_on: 3
# state_off: 4
# sensors:
# # Status bus VE
# # 0=Off;1=Low Power;2=Fault;3=Bulk;4=Absorption;5=Float;6=Storage;
# # 7=Equalize;8=Passthru;9=Inverting;10=Power assist;11=Power supply;252=External control
# - name: "MP2 Status bus VE"
# unique_id: "mp2_status_bus_ve"
# slave: 227
# address: 31
# data_type: uint16
# scale: 1
Palettes utilisées
Pour reproduire ce flow, installer :
- node-red-contrib-victron (entrées/sorties ESS vers Venus OS)
- palette VRM API (nœud
vrm-apiutilisé pour récupérer les stats/prévisions VRM) - node-red-contrib-bigtimer (horloge 22h → 6h)
- node-red-dashboard (ui_gauge, ui_text, ui_chart)
- node-red-node-mqtt (mqtt in/out)
Conclusion
Avec ce flow, je ne me contente plus de “collecter des données” : j’intègre la prévision (VRM), la décision (calcul cible SoC) et l’action (écriture CP1 dans Victron), tout en gardant une supervision propre via MQTT/Home Assistant.
Annexe JSON
Flux JSON à copier/coller :
[
{
"id": "5fe7408840b1cdf3",
"type": "tab",
"label": "DESS Rémy",
"disabled": false,
"info": "",
"env": []
},
{
"id": "58504cbc7c33f631",
"type": "group",
"z": "5fe7408840b1cdf3",
"style": {
"stroke": "#999999",
"stroke-opacity": "1",
"fill": "none",
"fill-opacity": "1",
"label": true,
"label-position": "nw",
"color": "#a4a4a4"
},
"nodes": [
"d0900f1bce2a4111",
"9a02956e8c37d773",
"6027c79e01cbefde",
"4f987019c9af7859",
"59baa58061ee6884",
"80bbf22798b0087c"
],
"x": 34,
"y": 59,
"w": 992,
"h": 142
},
{
"id": "c64c5ecbe0654837",
"type": "group",
"z": "5fe7408840b1cdf3",
"style": {
"stroke": "#999999",
"stroke-opacity": "1",
"fill": "none",
"fill-opacity": "1",
"label": true,
"label-position": "nw",
"color": "#a4a4a4"
},
"nodes": [
"3401e9994f1b41b7",
"2d3a9307be086227",
"8e2f3efe43139cbb",
"309cfd45dc936c74",
"3771889792a45c9c",
"30724406b2c3e9a6",
"fce8b570e7de2979",
"2f7797d5a59eb37f"
],
"x": 34,
"y": 219,
"w": 1032,
"h": 222
},
{
"id": "27c9a9a63c937a72",
"type": "group",
"z": "5fe7408840b1cdf3",
"style": {
"stroke": "#999999",
"stroke-opacity": "1",
"fill": "none",
"fill-opacity": "1",
"label": true,
"label-position": "nw",
"color": "#a4a4a4"
},
"nodes": [
"56c0dd1a9c9f59d5",
"30fc5ca1af8c2544",
"573c180d02c5fc31"
],
"x": 194,
"y": 1639,
"w": 492,
"h": 122
},
{
"id": "f7b3a04d7293fa0c",
"type": "group",
"z": "5fe7408840b1cdf3",
"style": {
"stroke": "#999999",
"stroke-opacity": "1",
"fill": "none",
"fill-opacity": "1",
"label": true,
"label-position": "nw",
"color": "#a4a4a4"
},
"nodes": [
"c3ef0fbdd807d8bc",
"a45f19bbc80e5a79",
"fda0c5fc9d3a3778",
"2b5dea20fe8df2e3",
"e95a6ca3e8f41607",
"1cd50a3b5905c265"
],
"x": 54,
"y": 1779,
"w": 732,
"h": 222
},
{
"id": "0ffa85f6b2707ef1",
"type": "group",
"z": "5fe7408840b1cdf3",
"style": {
"stroke": "#999999",
"stroke-opacity": "1",
"fill": "none",
"fill-opacity": "1",
"label": true,
"label-position": "nw",
"color": "#a4a4a4"
},
"nodes": [
"cc13594f0954243c",
"1ad72d51e46cbe39",
"22983449d6bd5095",
"ddbae0defddeb15a",
"5f4b44a3e04b450a",
"35c49a11dd96c53d",
"b8be24fef26d691b",
"2b0b581c676e7291",
"7dbb8d64c879169d",
"63fe66bb76b0356c",
"4dbdbae84b4d7390",
"059db6a88f526987",
"cfd34293e830d3cd"
],
"x": 34,
"y": 1319,
"w": 832,
"h": 302
},
{
"id": "f8afda3620427140",
"type": "group",
"z": "5fe7408840b1cdf3",
"style": {
"stroke": "#999999",
"stroke-opacity": "1",
"fill": "none",
"fill-opacity": "1",
"label": true,
"label-position": "nw",
"color": "#a4a4a4"
},
"nodes": [
"0af06d70cb632e52",
"658208f94208bd9f",
"f7ddcf1e407745e9",
"67bc6865c7406fee",
"701744df73f35cf2",
"b70ceb2c221e3bd2",
"a0b8045a6d72c0eb",
"071b658e8fc627e4",
"6d096b019c095341",
"233a80856735ebfb",
"58133dfe4f341acc"
],
"x": 34,
"y": 1019,
"w": 1032,
"h": 282
},
{
"id": "9669903f47819092",
"type": "group",
"z": "5fe7408840b1cdf3",
"style": {
"stroke": "#999999",
"stroke-opacity": "1",
"fill": "none",
"fill-opacity": "1",
"label": true,
"label-position": "nw",
"color": "#a4a4a4"
},
"nodes": [
"9cf84313ff660683",
"4047bb58bd817588",
"e23bf7ba06f7abf5",
"38b5d65f62960b22",
"cbe71d4c64597512",
"c7bade8283007b6b",
"31630e5509a5a01c",
"665a65c2ac7c84ce",
"84e18d2c39c07038",
"7c1b7bd28ca87a43",
"e63917c57e76284a",
"5eacf6a66623ab28",
"5ddb73a1d5111255",
"d9d9937809ad2185",
"7a72bd69fea8ec17",
"f5bd2bcbcc8eb176",
"7b1ac5814db8a9ac",
"4b84dbc2655e1bcd",
"a8b2fec6fdd2265b",
"ce7397f52af03a02",
"8ef38bc25af154c7",
"6c53e12e5fa4b06e",
"733ab2eac5f9cbac",
"be13222435c38a50",
"83b23b5304a18717",
"4654dd1c8bf3344e",
"317a0cfb20712290",
"8869e559a62cac58"
],
"x": 34,
"y": 479,
"w": 1132,
"h": 522
},
{
"id": "0af06d70cb632e52",
"type": "change",
"z": "5fe7408840b1cdf3",
"g": "f8afda3620427140",
"name": "SOC",
"rules": [
{
"t": "set",
"p": "soc",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 310,
"y": 1260,
"wires": [
[
"67bc6865c7406fee"
]
]
},
{
"id": "d0900f1bce2a4111",
"type": "change",
"z": "5fe7408840b1cdf3",
"g": "58504cbc7c33f631",
"name": "Save previ_prod",
"rules": [
{
"t": "set",
"p": "previ_prod",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 700,
"y": 160,
"wires": [
[
"9a02956e8c37d773",
"2d3a9307be086227"
]
]
},
{
"id": "9cf84313ff660683",
"type": "comment",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"name": "Calcul du niveau SOC pour la charge programmée",
"info": "",
"x": 590,
"y": 520,
"wires": []
},
{
"id": "4047bb58bd817588",
"type": "mqtt out",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"name": "Cible SOC",
"topic": "mp2/dess_remy/cible_soc",
"qos": "",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "502248144035edbf",
"x": 710,
"y": 920,
"wires": []
},
{
"id": "e23bf7ba06f7abf5",
"type": "function",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"name": "Pilotage CP1 V6",
"func": "// ===================================================================\n// 🔋 Pilotage CP1 V6 : gestion charge programmée 22h–6h\n// Garde le calcul du SoC théorique,\n// ===================================================================\n\n// === Lecture des variables ===\nlet soc = flow.get('soc'); // SoC actuel (%)\nlet prod = flow.get('previ_prod'); // Prévision de production (kWh)\nlet f100 = flow.get('forc100'); // Forçage manuel (on/off)\nlet nfcp1 = flow.get('niveauforcp1'); // Niveau CP1 forcé\nlet havalid = global.get('valid_cp_ess'); // Autorisation HA (\"on\"/true)\nlet hdeb = flow.get('hdebut'); // Heure début HC (min)\nlet hfin = flow.get('hfin'); // Heure fin HC (min)\nlet horloge = flow.get('horloge'); // 1 = fenêtre 22h–6h\nlet soc_cible_calc = Number(flow.get('dess_soc_cible_pct')); // SoC cible calculé via DoD J+1\n\n// Normalisation HA_ON\nconst HA_ON = (havalid === \"on\" || havalid === true || havalid === 1);\n\n// === Constantes ===\nconst a = 120; // Base de calcul du SoC cible\nconst b = 2.5; // Pondération selon la prévision\n\n// === Variables locales ===\nlet seuil = 0;\nlet valid = { payload: 7 };\nlet devalid = { payload: -1 };\nlet debug = {};\nlet duree = {};\nlet hDebutS = {};\nlet autoconsoMsg = { payload: 0 }; // 1->PV+Batterie / 0->PV\n\n// ===================================================================\n// Utils HH:MM depuis minutes (0..1439), robuste\n// ===================================================================\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// ===================================================================\n// 1️⃣ Calcul du SEUIL cible SoC\n// Priorité : SoC cible calculé (DoD J+1) -> sinon fallback a - b*prod\n// ===================================================================\n\n// Fallback historique (si pas de calcul DoD dispo)\nseuil = a - (b * prod);\n\n// Si le SoC cible calculé est valide, il remplace le fallback\nif (Number.isFinite(soc_cible_calc) && soc_cible_calc > 0) {\n seuil = soc_cible_calc;\n}\n\n// Forçage manuel prioritaire sur tout le reste\nif (f100 == 'on') {\n seuil = nfcp1;\n}\n\n// Arrondi / bornage\nseuil = Math.round(seuil);\nif (seuil < 15) seuil = 0;\nif (seuil > 100) seuil = 100;\n\nflow.set(\"seuil_soc\", seuil);\n\n// ===================================================================\n// 2️⃣ Calcul de la durée et heure début HC (pour compatibilité)\n// ===================================================================\nlet h_corr = Number(hdeb) / 60;\nlet fin = Number(hfin) / 60;\n\n// garde-fous simples\nif (!Number.isFinite(h_corr)) h_corr = 0;\nif (!Number.isFinite(fin)) fin = 0;\n\nlet tps = (h_corr > fin)\n ? (24 - h_corr + fin) * 3600\n : (fin - h_corr) * 3600;\n\nduree = { payload: tps };\nhDebutS = { payload: h_corr * 3600 };\n\n// Format durée HH:MM\nlet h = Math.floor(tps / 3600);\nlet m = Math.floor((tps % 3600) / 60);\nlet duree_hhmm = `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}`;\n\n// Heures début/fin en clair (HH:MM) depuis minutes\nconst hdeb_hhmm = hhmmFromMin(hdeb);\nconst hfin_hhmm = hhmmFromMin(hfin);\n\n// ===================================================================\n// 3️⃣ Sortie 1 : Activation charge programmée Victron (CP1)\n// Règle : on n'active CP1 que si SoC < Seuil (avec hystérésis)\n// Forçage f100 garde la priorité\n// ===================================================================\n\nconst HYST = 1.0; // %\n\nlet actif_payload = -1;\n\n// Normaliser SoC en nombre\nsoc = Number(soc);\nif (!Number.isFinite(soc)) soc = 0;\n\n// Décision CP1 hors forçage\nlet besoin_charge = false;\n\n// Si seuil==0 => on considère \"pas de cible\", donc pas de CP1 automatique\nif (seuil > 0) {\n const prev = (flow.get('ess_actif') === true);\n\n if (soc <= (seuil - HYST)) besoin_charge = true;\n else if (soc >= (seuil + HYST)) besoin_charge = false;\n else besoin_charge = prev;\n}\n\n// Forçage manuel prioritaire (si f100 ON, on active CP1 pendant la fenêtre HC)\nconst f100_actif = (f100 === 'on');\n\nif (HA_ON && horloge === 1) {\n if (f100_actif) {\n actif_payload = 7;\n } else {\n actif_payload = besoin_charge ? 7 : -1;\n }\n} else {\n actif_payload = -1;\n}\n\nlet ess_actif = (actif_payload === 7);\nflow.set('ess_actif', ess_actif);\n\n// ===================================================================\n// 4️⃣ Log synthétique (avec Hdeb/Hfin en clair)\n// ===================================================================\nlet f100_txt = f100_actif ? \"ON\" : \"OFF\";\nlet nfcp1_txt = f100_actif ? `${nfcp1}%` : \"-\";\nlet besoin_txt = f100_actif ? \"FORCE\" : (besoin_charge ? \"ON\" : \"OFF\");\n\nlet tag =\n (!HA_ON)\n ? \"🚫 HA OFF\"\n : (horloge === 1\n ? \"⚡ CP1 actif (22–6h)\"\n : \"⏱️ CP1 inactif\");\n\nlet logTxt =\n `${tag}` +\n ` | SoC=${soc}%` +\n ` | Seuil=${seuil}%` +\n ` | Hdeb=${hdeb_hhmm}` +\n ` | Hfin=${hfin_hhmm}` +\n ` | DuréeHC=${duree_hhmm}` +\n ` | Forçage=${f100_txt}` +\n ` | NiveauForcé=${nfcp1_txt}` +\n ` | Horloge=${horloge}` +\n ` | havalid=${havalid}` +\n ` | Sortie1=${actif_payload}` +\n ` | BesoinCP=${besoin_txt}` +\n ` | Hyst=${HYST}%`;\n\ndebug = { payload: logTxt };\n\n// ===================================================================\n// 5️⃣ Sorties Node-RED\n// ===================================================================\nlet seuilMsg = { payload: seuil };\n\nreturn [\n (actif_payload === 7) ? valid : devalid, // 1. Commande ESS (7 / -1)\n hDebutS, // 2. Heure de début (s)\n duree, // 3. Durée (s)\n debug, // 4. Log texte\n seuilMsg, // 5. Seuil SoC\n autoconsoMsg // 6. Autoconso placeholder\n];\n",
"outputs": 6,
"timeout": "",
"noerr": 0,
"initialize": "// Le code ajouté ici sera exécuté une fois\n// à chaque démarrage du noeud.\nflow.set('cligno', \"off\");",
"finalize": "",
"libs": [],
"x": 560,
"y": 780,
"wires": [
[
"7a72bd69fea8ec17",
"cbe71d4c64597512"
],
[
"c7bade8283007b6b",
"317a0cfb20712290"
],
[
"31630e5509a5a01c",
"4654dd1c8bf3344e"
],
[
"d9d9937809ad2185",
"7b1ac5814db8a9ac"
],
[
"38b5d65f62960b22",
"4047bb58bd817588",
"4b84dbc2655e1bcd"
],
[
"f5bd2bcbcc8eb176"
]
],
"outputLabels": [
"Validation",
"Heure Début",
"Durée",
"Log",
"Ecart SOC",
"H Début"
]
},
{
"id": "38b5d65f62960b22",
"type": "victron-output-ess",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"service": "com.victronenergy.settings",
"path": "/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Soc",
"serviceObj": {
"service": "com.victronenergy.settings",
"name": "Venus settings"
},
"pathObj": {
"path": "/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Soc",
"type": "integer",
"name": "Schedule 1: State of charge (%)",
"writable": true
},
"initial": "",
"name": "Cible SOC ",
"onlyChanges": false,
"x": 890,
"y": 900,
"wires": []
},
{
"id": "cbe71d4c64597512",
"type": "victron-output-ess",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"service": "com.victronenergy.settings",
"path": "/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Day",
"serviceObj": {
"service": "com.victronenergy.settings",
"name": "Venus settings"
},
"pathObj": {
"path": "/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Day",
"type": "enum",
"name": "Schedule 1: Day",
"enum": {
"0": "Sunday",
"1": "Monday",
"2": "Tuesday",
"3": "Wednesday",
"4": "Thursday",
"5": "Friday",
"6": "Saturday",
"7": "Every day",
"8": "Weekdays",
"9": "Weekends",
"11": "Monthly",
"-1": "Disabled"
},
"remarks": "<p>A negative value means that the schedule has been de-activated.</p>",
"mode": "both"
},
"name": "Validation CP",
"onlyChanges": false,
"x": 800,
"y": 560,
"wires": []
},
{
"id": "c7bade8283007b6b",
"type": "victron-output-ess",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"service": "com.victronenergy.settings",
"path": "/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Start",
"serviceObj": {
"service": "com.victronenergy.settings",
"name": "Venus settings"
},
"pathObj": {
"path": "/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Start",
"type": "integer",
"name": "Schedule 1: Start (seconds after midnight)",
"writable": true
},
"initial": "",
"name": "Heure Début",
"onlyChanges": false,
"x": 770,
"y": 720,
"wires": []
},
{
"id": "31630e5509a5a01c",
"type": "victron-output-ess",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"service": "com.victronenergy.settings",
"path": "/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Duration",
"serviceObj": {
"service": "com.victronenergy.settings",
"name": "Venus settings"
},
"pathObj": {
"path": "/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Duration",
"type": "integer",
"name": "Schedule 1: Duration (seconds)",
"writable": true
},
"initial": "",
"name": "Durée",
"onlyChanges": false,
"x": 790,
"y": 780,
"wires": []
},
{
"id": "665a65c2ac7c84ce",
"type": "bigtimer",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"outtopic": "",
"outpayload1": "",
"outpayload2": "",
"name": "CP 22->6",
"comment": "",
"lat": "43.91905434993742",
"lon": "2.1979451056884747",
"starttime": "1320",
"endtime": "360",
"starttime2": "0",
"endtime2": "0",
"startoff": 0,
"endoff": 0,
"startoff2": 0,
"endoff2": 0,
"offs": 0,
"outtext1": "\"on\"",
"outtext2": "\"off\"",
"timeout": 1440,
"sun": true,
"mon": true,
"tue": true,
"wed": true,
"thu": true,
"fri": true,
"sat": true,
"jan": true,
"feb": true,
"mar": true,
"apr": true,
"may": true,
"jun": true,
"jul": true,
"aug": true,
"sep": true,
"oct": true,
"nov": true,
"dec": true,
"day1": 0,
"month1": 0,
"day2": 0,
"month2": 0,
"day3": 0,
"month3": 0,
"day4": 0,
"month4": 0,
"day5": 0,
"month5": 0,
"day6": 0,
"month6": 0,
"day7": 0,
"month7": 0,
"day8": 0,
"month8": 0,
"day9": 0,
"month9": 0,
"day10": 0,
"month10": 0,
"day11": 0,
"month11": 0,
"day12": 0,
"month12": 0,
"d1": 0,
"w1": 0,
"d2": 0,
"w2": 0,
"d3": 0,
"w3": 0,
"d4": 0,
"w4": 0,
"d5": 0,
"w5": 0,
"d6": 0,
"w6": 0,
"xday1": 0,
"xmonth1": 0,
"xday2": 0,
"xmonth2": 0,
"xday3": 0,
"xmonth3": 0,
"xday4": 0,
"xmonth4": 0,
"xday5": 0,
"xmonth5": 0,
"xday6": 0,
"xmonth6": 0,
"xday7": 0,
"xmonth7": 0,
"xday8": 0,
"xmonth8": 0,
"xday9": 0,
"xmonth9": 0,
"xday10": 0,
"xmonth10": 0,
"xday11": 0,
"xmonth11": 0,
"xday12": 0,
"xmonth12": 0,
"xd1": 0,
"xw1": 0,
"xd2": 0,
"xw2": 0,
"xd3": 0,
"xw3": 0,
"xd4": 0,
"xw4": 0,
"xd5": 0,
"xw5": 0,
"xd6": 0,
"xw6": 0,
"suspend": false,
"random": false,
"randon1": false,
"randoff1": false,
"randon2": false,
"randoff2": false,
"repeat": true,
"atstart": true,
"odd": false,
"even": false,
"x": 160,
"y": 880,
"wires": [
[],
[
"8869e559a62cac58"
],
[]
]
},
{
"id": "84e18d2c39c07038",
"type": "change",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"name": "F100",
"rules": [
{
"t": "set",
"p": "forc100",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 330,
"y": 760,
"wires": [
[
"e23bf7ba06f7abf5"
]
]
},
{
"id": "7c1b7bd28ca87a43",
"type": "change",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"name": "VCP",
"rules": [
{
"t": "set",
"p": "valid_cp_ess",
"pt": "global",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 330,
"y": 620,
"wires": [
[
"e23bf7ba06f7abf5"
]
]
},
{
"id": "e63917c57e76284a",
"type": "change",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"name": "NF CP1",
"rules": [
{
"t": "set",
"p": "niveauforcp1",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 340,
"y": 700,
"wires": [
[
"e23bf7ba06f7abf5"
]
]
},
{
"id": "5eacf6a66623ab28",
"type": "mqtt out",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"name": "Valid CP",
"topic": "mp2/multiplus2/valide_cp",
"qos": "",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "502248144035edbf",
"x": 1020,
"y": 600,
"wires": []
},
{
"id": "5ddb73a1d5111255",
"type": "mqtt out",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"name": "H DEBUT",
"topic": "mp2/dess_remy/h_debut",
"qos": "",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "502248144035edbf",
"x": 1080,
"y": 720,
"wires": []
},
{
"id": "d9d9937809ad2185",
"type": "debug",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"name": "debug 62",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "true",
"targetType": "full",
"statusVal": "payload",
"statusType": "auto",
"x": 1040,
"y": 860,
"wires": []
},
{
"id": "7a72bd69fea8ec17",
"type": "function",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"name": "Convert",
"func": "var code = msg.payload;\n\n// Tableau de correspondance code → texte\nvar mapper = {\n \"-1\": \"off\",\n \"7\": \"on\"\n};\n\n// Fonction de mapping\nfunction extraireTexte(valeur) {\n return mapper[valeur.toString()] || \"Etat inconnu\";\n}\n\n// Application du mapping\nvar texte = extraireTexte(code);\nmsg.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 msg;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 800,
"y": 640,
"wires": [
[
"5eacf6a66623ab28"
]
]
},
{
"id": "658208f94208bd9f",
"type": "ui_chart",
"z": "5fe7408840b1cdf3",
"g": "f8afda3620427140",
"name": "",
"group": "c8a7d12bb7f76f27",
"order": 14,
"width": 0,
"height": 0,
"label": "SOC",
"chartType": "line",
"legend": "false",
"xformat": "HH:mm:ss",
"interpolate": "linear",
"nodata": "",
"dot": false,
"ymin": "0",
"ymax": "100",
"removeOlder": "2",
"removeOlderPoints": "",
"removeOlderUnit": "3600",
"cutout": 0,
"useOneColor": false,
"useUTC": true,
"colors": [
"#1f77b4",
"#aec7e8",
"#ff7f0e",
"#2ca02c",
"#98df8a",
"#d62728",
"#ff9896",
"#9467bd",
"#c5b0d5"
],
"outputs": 1,
"useDifferentColor": false,
"className": "",
"x": 990,
"y": 1260,
"wires": [
[]
]
},
{
"id": "f5bd2bcbcc8eb176",
"type": "victron-output-ess",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"service": "com.victronenergy.settings",
"path": "/Settings/CGwacs/BatteryLife/Schedule/Charge/0/AllowDischarge",
"serviceObj": {
"service": "com.victronenergy.settings",
"name": "Venus settings"
},
"pathObj": {
"path": "/Settings/CGwacs/BatteryLife/Schedule/Charge/0/AllowDischarge",
"type": "enum",
"name": "Schedule 1: Self-consumption above limit",
"enum": {
"0": "Yes",
"1": "No"
},
"mode": "both"
},
"initial": 0,
"name": "Autoconsommation",
"onlyChanges": false,
"x": 930,
"y": 960,
"wires": []
},
{
"id": "7b1ac5814db8a9ac",
"type": "ui_text",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"group": "c8a7d12bb7f76f27",
"order": 3,
"width": "12",
"height": "2",
"name": "",
"label": "Log CP1:",
"format": "{{msg.payload}}",
"layout": "row-spread",
"className": "",
"style": false,
"font": "",
"fontSize": 16,
"color": "#000000",
"x": 880,
"y": 860,
"wires": []
},
{
"id": "9a02956e8c37d773",
"type": "ui_gauge",
"z": "5fe7408840b1cdf3",
"g": "58504cbc7c33f631",
"name": "",
"group": "c8a7d12bb7f76f27",
"order": 12,
"width": "3",
"height": "3",
"gtype": "gage",
"title": "Previ Prod J",
"label": "kWh",
"format": "{{value}}",
"min": 0,
"max": "30",
"colors": [
"#00b500",
"#e6e600",
"#ca3838"
],
"seg1": "",
"seg2": "",
"diff": false,
"className": "",
"x": 930,
"y": 160,
"wires": []
},
{
"id": "f7ddcf1e407745e9",
"type": "victron-input-battery",
"z": "5fe7408840b1cdf3",
"g": "f8afda3620427140",
"service": "com.victronenergy.battery/277",
"path": "/Soc",
"serviceObj": {
"service": "com.victronenergy.battery/277",
"name": "SmartShunt 500A/50mV"
},
"pathObj": {
"path": "/Soc",
"type": "float",
"name": "State of charge (%)"
},
"initial": "",
"name": "SOC Batteries",
"onlyChanges": false,
"roundValues": "2",
"x": 150,
"y": 1260,
"wires": [
[
"0af06d70cb632e52"
]
]
},
{
"id": "4b84dbc2655e1bcd",
"type": "ui_gauge",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"name": "",
"group": "c8a7d12bb7f76f27",
"order": 10,
"width": "3",
"height": "3",
"gtype": "gage",
"title": "SOC Cible",
"label": "%",
"format": "{{value}}",
"min": 0,
"max": "100",
"colors": [
"#00b500",
"#e6e600",
"#ca3838"
],
"seg1": "",
"seg2": "",
"diff": false,
"className": "",
"x": 1070,
"y": 920,
"wires": []
},
{
"id": "6027c79e01cbefde",
"type": "inject",
"z": "5fe7408840b1cdf3",
"g": "58504cbc7c33f631",
"name": "21:45",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "45 21 * * *",
"once": true,
"onceDelay": "5",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 140,
"y": 160,
"wires": [
[
"4f987019c9af7859"
]
]
},
{
"id": "80bbf22798b0087c",
"type": "comment",
"z": "5fe7408840b1cdf3",
"g": "58504cbc7c33f631",
"name": "Récupération de la production PV estimée par VRM Victron",
"info": "",
"x": 530,
"y": 100,
"wires": []
},
{
"id": "a8b2fec6fdd2265b",
"type": "mqtt in",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"name": "Forcage CP 100%",
"topic": "ha/mp2/cp/forcage100",
"qos": "2",
"datatype": "auto-detect",
"broker": "502248144035edbf",
"nl": false,
"rap": true,
"rh": 0,
"inputs": 0,
"x": 150,
"y": 760,
"wires": [
[
"84e18d2c39c07038",
"733ab2eac5f9cbac"
]
]
},
{
"id": "ce7397f52af03a02",
"type": "mqtt in",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"name": "Valid ESS MP1",
"topic": "ha/mp2/cp/validcp",
"qos": "2",
"datatype": "auto-detect",
"broker": "502248144035edbf",
"nl": false,
"rap": true,
"rh": 0,
"inputs": 0,
"x": 140,
"y": 620,
"wires": [
[
"be13222435c38a50",
"7c1b7bd28ca87a43"
]
]
},
{
"id": "8ef38bc25af154c7",
"type": "mqtt in",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"name": "Niveau Forcage CP1",
"topic": "ha/mp2/cp/niveauforcagecp1",
"qos": "2",
"datatype": "auto-detect",
"broker": "502248144035edbf",
"nl": false,
"rap": true,
"rh": 0,
"inputs": 0,
"x": 150,
"y": 700,
"wires": [
[
"e63917c57e76284a",
"6c53e12e5fa4b06e"
]
]
},
{
"id": "6c53e12e5fa4b06e",
"type": "debug",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"name": "debug 69",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "payload",
"targetType": "msg",
"statusVal": "payload",
"statusType": "auto",
"x": 540,
"y": 620,
"wires": []
},
{
"id": "733ab2eac5f9cbac",
"type": "debug",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"name": "debug 70",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "payload",
"targetType": "msg",
"statusVal": "payload",
"statusType": "auto",
"x": 540,
"y": 680,
"wires": []
},
{
"id": "be13222435c38a50",
"type": "debug",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"name": "debug 71",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "payload",
"targetType": "msg",
"statusVal": "payload",
"statusType": "auto",
"x": 520,
"y": 560,
"wires": []
},
{
"id": "83b23b5304a18717",
"type": "mqtt out",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"name": "Durée",
"topic": "mp2/dess_remy/duree",
"qos": "",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "502248144035edbf",
"x": 1070,
"y": 780,
"wires": []
},
{
"id": "4654dd1c8bf3344e",
"type": "function",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"name": "Convert",
"func": "// Entrée attendue : msg.payload = durée en secondes (number ou string)\n// Sortie : msg.payload = \"HH:MM\" (string)\n\nlet tps = Number(msg.payload);\n\n// Garde-fous\nif (!isFinite(tps) || tps < 0) {\n node.status({\n fill: \"red\",\n shape: \"ring\",\n text: \"Durée 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')}`;\nmsg.payload = hhmm;\n\n// Status Node-RED\nnode.status({\n fill: \"blue\",\n shape: \"dot\",\n text: `Durée = ${hhmm} (${tps}s)`\n});\n\n// Optionnel : pour tracer\n// msg.topic = \"ton/topic/mqtt/duree_hhmm\";\n\nreturn msg;\n\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 940,
"y": 780,
"wires": [
[
"83b23b5304a18717"
]
]
},
{
"id": "317a0cfb20712290",
"type": "function",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"name": "Convert",
"func": "// Entrée attendue : msg.payload = durée en secondes (number ou string)\n// Sortie : msg.payload = \"HH:MM\" (string)\n\nlet tps = Number(msg.payload);\n\n// Garde-fous\nif (!isFinite(tps) || tps < 0) {\n node.status({\n fill: \"red\",\n shape: \"ring\",\n text: \"H Début 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')}`;\nmsg.payload = hhmm;\n\n// Affichage dans le status du node\nnode.status({\n fill: \"blue\",\n shape: \"dot\",\n text: `H Début = ${hhmm} (${tps}s)`\n});\n\nreturn msg;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 920,
"y": 720,
"wires": [
[
"5ddb73a1d5111255"
]
]
},
{
"id": "67bc6865c7406fee",
"type": "function",
"z": "5fe7408840b1cdf3",
"g": "f8afda3620427140",
"name": "SOC ts les 15mn",
"func": "// Emission SoC toutes les 15 minutes (sans condition horloge)\n// - Au maximum 1 fois toutes les 15 minutes\n// - Met à jour flow.soc et laisse passer msg\n\nconst PERIOD_S = 15 * 60; // 15 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éjà 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 < 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 → mémorisation\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éfère msg.payload\nreturn msg;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 530,
"y": 1260,
"wires": [
[
"a0b8045a6d72c0eb",
"658208f94208bd9f",
"6d096b019c095341"
]
]
},
{
"id": "701744df73f35cf2",
"type": "ui_text",
"z": "5fe7408840b1cdf3",
"g": "f8afda3620427140",
"group": "c8a7d12bb7f76f27",
"order": 2,
"width": "12",
"height": "2",
"name": "",
"label": "Log H Dyn:",
"format": "{{msg.payload}}",
"layout": "row-spread",
"className": "",
"style": false,
"font": "",
"fontSize": 16,
"color": "#000000",
"x": 690,
"y": 1160,
"wires": []
},
{
"id": "2d3a9307be086227",
"type": "vrm-api",
"z": "5fe7408840b1cdf3",
"g": "c64c5ecbe0654837",
"vrm": "5e3a89e5eaeb1cb4",
"name": "VRM Conso J+1",
"api_type": "installations",
"idUser": "",
"idSite": "223181",
"installations": "stats",
"attribute": "vrm_consumption_fc",
"stats_interval": "hours",
"show_instance": false,
"stats_start": "bot",
"stats_end": "86400",
"use_utc": false,
"gps_start": "",
"gps_end": "",
"instance": "",
"vrm_id": "",
"b_max": "",
"tb_max": "",
"fb_max": "",
"tg_max": "",
"fg_max": "",
"b_cycle_cost": "",
"buy_price_formula": "",
"sell_price_formula": "",
"green_mode_on": true,
"feed_in_possible": true,
"feed_in_control_on": true,
"b_goal_hour": "",
"b_goal_SOC": "",
"store_in_global_context": false,
"verbose": false,
"x": 140,
"y": 320,
"wires": [
[
"8e2f3efe43139cbb"
]
]
},
{
"id": "3401e9994f1b41b7",
"type": "comment",
"z": "5fe7408840b1cdf3",
"g": "c64c5ecbe0654837",
"name": "Récupération de Consommation J+1 estimée par Victron et Calcul SOC Cible",
"info": "",
"x": 550,
"y": 260,
"wires": []
},
{
"id": "309cfd45dc936c74",
"type": "change",
"z": "5fe7408840b1cdf3",
"g": "c64c5ecbe0654837",
"name": "Conso_j1",
"rules": [
{
"t": "set",
"p": "conso_j1",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 540,
"y": 320,
"wires": [
[
"30724406b2c3e9a6",
"3771889792a45c9c"
]
],
"info": "*** mermaid\njourney\n title My working day\n section Go to work\n Make tea: 5: Me\n Go upstairs: 3: Me\n Do work: 1: Me, Cat\n section Go home\n Go downstairs: 5: Me\n Sit down: 5: Me"
},
{
"id": "fce8b570e7de2979",
"type": "ui_text",
"z": "5fe7408840b1cdf3",
"g": "c64c5ecbe0654837",
"group": "c8a7d12bb7f76f27",
"order": 1,
"width": "12",
"height": "2",
"name": "",
"label": "Cible Soc :",
"format": "{{msg.payload}}",
"layout": "row-spread",
"className": "",
"style": false,
"font": "",
"fontSize": 16,
"color": "#000000",
"x": 970,
"y": 320,
"wires": []
},
{
"id": "b70ceb2c221e3bd2",
"type": "inject",
"z": "5fe7408840b1cdf3",
"g": "f8afda3620427140",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "2",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 150,
"y": 1200,
"wires": [
[
"6d096b019c095341"
]
]
},
{
"id": "a0b8045a6d72c0eb",
"type": "ui_gauge",
"z": "5fe7408840b1cdf3",
"g": "f8afda3620427140",
"name": "",
"group": "c8a7d12bb7f76f27",
"order": 8,
"width": "3",
"height": "3",
"gtype": "gage",
"title": "SOC Calib",
"label": "%",
"format": "{{value}}",
"min": 0,
"max": "100",
"colors": [
"#00b500",
"#e6e600",
"#ca3838"
],
"seg1": "",
"seg2": "",
"diff": false,
"className": "",
"x": 810,
"y": 1260,
"wires": []
},
{
"id": "8e2f3efe43139cbb",
"type": "function",
"z": "5fe7408840b1cdf3",
"g": "c64c5ecbe0654837",
"name": "Extraction 6-22h",
"func": "// ===================================================================\n// 🔋 Prévision de consommation 6h–22h (jour J+1) à partir du JSON VRM\n// Exemple de données :\n// \"records\": { \"vrm_consumption_fc\": [[timestamp, Wh], [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(\"⚠️ Données '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(\"⚠️ Série 'vrm_consumption_fc' vide ou invalide !\");\n msg.payload = null;\n return msg;\n}\n\n// ===================================================================\n// 1️⃣ Détermination 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() &&\n a.getMonth() === b.getMonth() &&\n a.getDate() === b.getDate();\n}\n\n// ===================================================================\n// 2️⃣ Somme des valeurs entre 6h00 et 22h00 (jour J+1)\n// ===================================================================\nlet sommeWh = 0;\nfor (let i = 0; i < series.length; i++) {\n let timestamp = series[i][0];\n let valeurWh = series[i][1];\n if (!timestamp || isNaN(valeurWh)) continue;\n\n let date = new Date(timestamp);\n let h = date.getHours();\n\n // Vérifie que c'est le jour J+1 et dans la fenêtre 6–22h\n if (sameYMD(date, tomorrow) && h >= 6 && h < 22) {\n sommeWh += valeurWh;\n }\n}\n\n// ===================================================================\n// 3️⃣ Conversion Wh → kWh et arrondi\n// ===================================================================\nlet total_kWh = Math.round((sommeWh / 1000) * 10) / 10; // 1 décimale\n\n// ===================================================================\n// 4️⃣ 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émorisation en flow\n\nnode.status({ fill: \"green\", shape: \"dot\", text: `Prévi conso 6–22h = ${total_kWh} kWh` });\n\nreturn msg;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 340,
"y": 320,
"wires": [
[
"309cfd45dc936c74"
]
]
},
{
"id": "4f987019c9af7859",
"type": "vrm-api",
"z": "5fe7408840b1cdf3",
"g": "58504cbc7c33f631",
"vrm": "5e3a89e5eaeb1cb4",
"name": "Prod J+1",
"api_type": "installations",
"idUser": "",
"idSite": "223181",
"installations": "stats",
"attribute": "solar_yield_forecast",
"stats_interval": "hours",
"show_instance": false,
"stats_start": "bot",
"stats_end": "86400",
"use_utc": false,
"gps_start": "",
"gps_end": "",
"instance": "",
"vrm_id": "",
"b_max": "",
"tb_max": "",
"fb_max": "",
"tg_max": "",
"fg_max": "",
"b_cycle_cost": "",
"buy_price_formula": "",
"sell_price_formula": "",
"green_mode_on": true,
"feed_in_possible": true,
"feed_in_control_on": true,
"b_goal_hour": "",
"b_goal_SOC": "",
"store_in_global_context": false,
"verbose": false,
"x": 300,
"y": 160,
"wires": [
[
"59baa58061ee6884"
]
]
},
{
"id": "59baa58061ee6884",
"type": "function",
"z": "5fe7408840b1cdf3",
"g": "58504cbc7c33f631",
"name": "Extrac 6:22",
"func": "// ===================================================================\n// ☀️ Prévision PV J+1 entre 06:00 et 22:00 (kWh)\n// Source : records.solar_yield_forecast = [[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(\"⚠️ Données '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(\"⚠️ Série '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étermination 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() &&\n a.getMonth() === b.getMonth() &&\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 < 2) continue;\n\n const ts = row[0];\n const wh = Number(row[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) && h >= 6 && h < 22) {\n sumWh += wh;\n kept++;\n }\n}\n\n// -------------------------------------------------------------------\n// 3) Conversion Wh -> kWh (1 décimale)\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); // <- clé à 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–22 = ${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",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 490,
"y": 160,
"wires": [
[
"d0900f1bce2a4111"
]
]
},
{
"id": "30724406b2c3e9a6",
"type": "function",
"z": "5fe7408840b1cdf3",
"g": "c64c5ecbe0654837",
"name": "Calcul Soc Cible V3",
"func": "// ============================================\n// DESS perso - Calcul SoC cible @06:00 (V3 simple & cohérent)\n// Sorties :\n// 1) payload objet (dashboard / MQTT)\n// 2) log texte lisible (dashboard)\n// 3) log texte HA (fichier)\n// ============================================\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// -------- PARAMETRES BATTERIE --------\nconst batt_nom_kWh = 12.0; // capacité utile réelle\nconst soc_min_pct = 8; // plancher dur\nconst soc_max_pct = 100;\nconst eta = 0.92; // rendement global\n\n// -------- MARGES ----------\nconst marge_kWh = 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}\n\n// -------- VALIDATION ----------\nif (![conso_kWh, pv_kWh, socNow].every(ok)) {\n node.status({ fill: \"red\", shape: \"ring\", text: \"Entrees manquantes\" });\n\n const now = new Date();\n const txt = `DESS_PERSO @ ${hhmm(now)} | ERREUR entrees | conso_j1=${flow.get('conso_j1')} | pv=${flow.get('previ_prod')} | soc=${flow.get('soc')}`;\n\n return [\n null,\n { payload: txt },\n { payload: txt }\n ];\n}\n\nsocNow = clamp(socNow, 0, 100);\n\n// -------- CAPACITES ----------\nconst socMin = clamp(soc_min_pct, 0, 99);\nconst socMax = clamp(soc_max_pct, socMin + 1, 100);\n\nconst batt_utile_kWh =\n Math.max(0.01, batt_nom_kWh * (socMax - socMin) / 100) * eta;\n\nconst fracNow = clamp((socNow - socMin) / (socMax - socMin), 0, 1);\nconst energie_dispo_kWh = fracNow * batt_utile_kWh;\n\n// -------- BESOIN ----------\nlet deficit_kWh = (conso_kWh - pv_kWh) + marge_kWh;\ndeficit_kWh = Math.max(0, deficit_kWh);\n\nconst dod = clamp(deficit_kWh / batt_utile_kWh, 0, 1);\n\nlet soc_cible = socMin + dod * (socMax - socMin);\nsoc_cible = clamp(soc_cible + marge_soc_pct, socMin, socMax);\n\nconst fracCible = clamp((soc_cible - socMin) / (socMax - socMin), 0, 1);\nconst energie_cible_kWh = fracCible * batt_utile_kWh;\n\nconst energie_a_charger_kWh = Math.max(0, energie_cible_kWh - energie_dispo_kWh);\nconst soc_a_charger_pct = clamp(soc_cible - socNow, 0, socMax - socMin);\n\n// -------- SORTIE 1 : OBJET ----------\nconst payload = {\n conso_kWh: Number(conso_kWh.toFixed(2)),\n pv_kWh: Number(pv_kWh.toFixed(2)),\n deficit_kWh: Number(deficit_kWh.toFixed(2)),\n\n soc_now_pct: Number(socNow.toFixed(1)),\n soc_min_pct: socMin,\n soc_cible_pct: Number(soc_cible.toFixed(1)),\n soc_a_charger_pct: Number(soc_a_charger_pct.toFixed(1)),\n\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: Number(energie_a_charger_kWh.toFixed(2))\n};\n\nflow.set('dess_soc_cible_pct', payload.soc_cible_pct);\nflow.set('dess_dod_pct', Number((dod * 100).toFixed(1)));\n\nmsg.payload = payload;\n\n// -------- SORTIE 2 : LOG DASHBOARD ----------\nconst now = new Date();\nconst deltaSoc = payload.soc_cible_pct - payload.soc_now_pct;\n\nconst logDash =\n `SOC Cible @ ${hhmm(now)}` +\n ` | SoC=${payload.soc_now_pct}%→${payload.soc_cible_pct}% (Δ=${deltaSoc.toFixed(1)}%)` +\n ` | conso=${payload.conso_kWh}kWh pv=${payload.pv_kWh}kWh` +\n ` | a_charger=${payload.energie_a_charger_kWh}kWh`;\n\n\n// -------- SORTIE 3 : LOG HA (FICHIER) ----------\nconst logHa =\n `SOC Cible @ ${hhmm(now)}` +\n ` | SoC=${payload.soc_now_pct}% → cible=${payload.soc_cible_pct}%` +\n ` | conso_J1=${payload.conso_kWh}kWh` +\n ` | pv_J1=${payload.pv_kWh}kWh` +\n ` | deficit=${payload.deficit_kWh}kWh` +\n ` | batt_utile=${payload.batt_utile_kWh}kWh` +\n ` | dispo=${payload.energie_dispo_kWh}kWh` +\n ` | a_charger=${payload.energie_a_charger_kWh}kWh` +\n ` | soc_a_charger=${payload.soc_a_charger_pct}%` +\n ` | DoD=${(dod * 100).toFixed(1)}%`;\n\n// -------- STATUS ----------\nnode.status({\n fill: energie_a_charger_kWh > 0.05 ? \"yellow\" : \"green\",\n shape: \"dot\",\n text: `SoC cible=${payload.soc_cible_pct}% | a charger=${payload.energie_a_charger_kWh}kWh`\n});\n\n// -------- RETOUR ----------\nreturn [\n msg,\n { payload: logDash },\n { payload: logHa }\n];\n",
"outputs": 3,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 760,
"y": 320,
"wires": [
[],
[
"fce8b570e7de2979"
],
[
"2f7797d5a59eb37f"
]
]
},
{
"id": "cc13594f0954243c",
"type": "debug",
"z": "5fe7408840b1cdf3",
"g": "0ffa85f6b2707ef1",
"name": "debug 101",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "true",
"targetType": "full",
"statusVal": "payload",
"statusType": "auto",
"x": 750,
"y": 1400,
"wires": []
},
{
"id": "1ad72d51e46cbe39",
"type": "mqtt in",
"z": "5fe7408840b1cdf3",
"g": "0ffa85f6b2707ef1",
"name": "Energie Reseau Jour",
"topic": "ha/mp2/cp/energie_reseau_jour",
"qos": "2",
"datatype": "auto-detect",
"broker": "502248144035edbf",
"nl": false,
"rap": true,
"rh": 0,
"inputs": 0,
"x": 160,
"y": 1400,
"wires": [
[
"22983449d6bd5095"
]
]
},
{
"id": "22983449d6bd5095",
"type": "change",
"z": "5fe7408840b1cdf3",
"g": "0ffa85f6b2707ef1",
"name": "grid_import_total_kwh",
"rules": [
{
"t": "set",
"p": "grid_import_total_kwh",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 460,
"y": 1400,
"wires": [
[
"cc13594f0954243c"
]
]
},
{
"id": "ddbae0defddeb15a",
"type": "function",
"z": "5fe7408840b1cdf3",
"g": "0ffa85f6b2707ef1",
"name": "Snaptshot 6:00",
"func": "// ================================\n// AUDIT DESS - SNAPSHOT 06:00\n// ================================\n\nconst soc = Number(flow.get('soc')); // SoC reel (%)\nconst grid = Number(flow.get('grid_import_total_kwh')); // compteur cumulatif kWh\n\nif (![soc, grid].every(Number.isFinite)) {\n node.status({ fill: \"red\", shape: \"ring\", text: \"Snapshot 06h invalide (soc/grid)\" });\n return null;\n}\n\n// Memorisation\nflow.set('audit_06h', {\n ts: Date.now(),\n soc_pct: Number(soc.toFixed(2)),\n grid_import_kwh: Number(grid.toFixed(3))\n});\n\nnode.status({\n fill: \"blue\",\n shape: \"dot\",\n text: `06h | SoC=${soc.toFixed(1)}% | Grid=${grid.toFixed(3)}kWh`\n});\n\nreturn null;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 480,
"y": 1460,
"wires": [
[]
]
},
{
"id": "5f4b44a3e04b450a",
"type": "inject",
"z": "5fe7408840b1cdf3",
"g": "0ffa85f6b2707ef1",
"name": "6:00",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "00 06 * * *",
"once": false,
"onceDelay": "2",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 210,
"y": 1460,
"wires": [
[
"ddbae0defddeb15a"
]
]
},
{
"id": "35c49a11dd96c53d",
"type": "function",
"z": "5fe7408840b1cdf3",
"g": "0ffa85f6b2707ef1",
"name": "Snaptshot 22:00",
"func": "// ================================\n// AUDIT DESS - SNAPSHOT 22:00\n// ================================\n\nconst soc = Number(flow.get('soc')); // SoC reel (%)\nconst grid = Number(flow.get('grid_import_total_kwh')); // compteur cumulatif kWh\n\nif (![soc, grid].every(Number.isFinite)) {\n node.status({ fill: \"red\", shape: \"ring\", text: \"Snapshot 22h invalide (soc/grid)\" });\n return null;\n}\n\n// Memorisation\nflow.set('audit_22h', {\n ts: Date.now(),\n soc_pct: Number(soc.toFixed(2)),\n grid_import_kwh: Number(grid.toFixed(3))\n});\n\nnode.status({\n fill: \"blue\",\n shape: \"dot\",\n text: `22h | SoC=${soc.toFixed(1)}% | Grid=${grid.toFixed(3)}kWh`\n});\n\nreturn null;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 480,
"y": 1500,
"wires": [
[]
]
},
{
"id": "b8be24fef26d691b",
"type": "inject",
"z": "5fe7408840b1cdf3",
"g": "0ffa85f6b2707ef1",
"name": "22:00",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "00 22 * * *",
"once": false,
"onceDelay": "5",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 220,
"y": 1500,
"wires": [
[
"35c49a11dd96c53d"
]
]
},
{
"id": "2b0b581c676e7291",
"type": "function",
"z": "5fe7408840b1cdf3",
"g": "0ffa85f6b2707ef1",
"name": "Snaptshot 22:05",
"func": "// ================================\n// AUDIT DESS - CALCUL JOURNALIER\n// Sorties :\n// 1) Objet audit (JSON)\n// 2) Log dashboard (texte)\n// ================================\n\n// Recuperation snapshots\nconst s6 = flow.get('audit_06h');\nconst s22 = flow.get('audit_22h');\n\nconst soc_cible = Number(flow.get('dess_soc_cible_pct')); // SoC cible calcule la veille\nconst soc_min = 8; // plancher dur (a ajuster)\n\n// Utils\nfunction hhmm(d) {\n const h = d.getHours();\n const m = d.getMinutes();\n return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}`;\n}\nfunction f1(x) { return Number(x).toFixed(1); }\nfunction f3(x) { return Number(x).toFixed(3); }\n\nif (!s6 || !s22 || !Number.isFinite(soc_cible)) {\n node.status({ fill: \"red\", shape: \"ring\", text: \"Audit incomplet (snapshots/cible)\" });\n\n const now = new Date();\n const logTxt =\n `AUDIT_DESS @ ${hhmm(now)}` +\n ` | ERREUR=Audit incomplet` +\n ` | have_s6=${!!s6}` +\n ` | have_s22=${!!s22}` +\n ` | soc_cible=${Number.isFinite(soc_cible) ? soc_cible : \"NaN\"}`;\n\n return [null, { payload: logTxt }];\n}\n\n// Calculs\nlet grid_import = s22.grid_import_kwh - s6.grid_import_kwh;\n\nif (!Number.isFinite(grid_import)) {\n node.status({ fill: \"red\", shape: \"ring\", text: \"Grid import invalide\" });\n\n const now = new Date();\n const logTxt =\n `AUDIT_DESS @ ${hhmm(now)}` +\n ` | ERREUR=Grid import invalide` +\n ` | s6.grid=${s6.grid_import_kwh}` +\n ` | s22.grid=${s22.grid_import_kwh}`;\n\n return [null, { payload: logTxt }];\n}\n\nif (grid_import < -0.01) {\n node.status({ fill: \"red\", shape: \"ring\", text: \"Reset compteur grid detecte\" });\n\n const now = new Date();\n const logTxt =\n `AUDIT_DESS @ ${hhmm(now)}` +\n ` | ERREUR=Reset compteur grid` +\n ` | grid_delta=${f3(grid_import)}kWh`;\n\n return [null, { payload: logTxt }];\n}\nconst SOC_TOL_LOW = 2; // si SoC06 < cible -> risque\nconst SOC_TOL_HIGH = 6; // si SoC06 > cible -> pas critique\n\nconst d06 = s6.soc_pct - soc_cible;\nconst ok_soc_06h = (d06 >= -SOC_TOL_LOW) && (d06 <= SOC_TOL_HIGH);\n\nconst ok_grid = grid_import <= 0.3;\nconst ok_soc_22h = s22.soc_pct >= (soc_min + 2);\n\n// Resultat audit\nconst audit = {\n soc_cible_pct: Number(soc_cible.toFixed(1)),\n\n soc_06h_pct: Number(s6.soc_pct.toFixed(1)),\n soc_22h_pct: Number(s22.soc_pct.toFixed(1)),\n\n grid_import_06_22_kwh: Number(grid_import.toFixed(3)),\n\n ok_soc_06h,\n ok_grid,\n ok_soc_22h,\n\n audit_ok: ok_soc_06h && ok_grid && ok_soc_22h\n};\n\n// Stockage\nflow.set('audit_result', audit);\n\n// Status clair\nconst nokFlags = [\n !ok_soc_06h && \"SoC06\",\n !ok_grid && \"Grid\",\n !ok_soc_22h && \"SoC22\"\n].filter(Boolean);\n\nnode.status({\n fill: audit.audit_ok ? \"green\" : \"yellow\",\n shape: \"dot\",\n text: audit.audit_ok ? \"Audit DESS OK\" : `Audit NOK (${nokFlags.join(\",\")})`\n});\n\n// -------- SORTIE 2 : LOG DASHBOARD ----------\nconst now = new Date();\nconst delta06 = (s6.soc_pct - soc_cible);\nconst tags = audit.audit_ok ? \"OK\" : `NOK:${nokFlags.join(\"+\")}`;\n\nconst logTxt =\n `AUDIT_DESS @ ${hhmm(now)}` +\n ` | cible=${f1(audit.soc_cible_pct)}%` +\n ` | SoC06=${f1(audit.soc_06h_pct)}% (Δ=${f1(delta06)}%)` +\n ` | SoC22=${f1(audit.soc_22h_pct)}%` +\n ` | Grid06-22=${f3(audit.grid_import_06_22_kwh)}kWh` +\n ` | checks=[SoC06:${ok_soc_06h ? \"OK\" : \"NOK\"},Grid:${ok_grid ? \"OK\" : \"NOK\"},SoC22:${ok_soc_22h ? \"OK\" : \"NOK\"}]` +\n ` | ${tags}`;\n\n// Sortie 1: objet audit\nmsg.payload = audit;\n\n// Retour 2 sorties\nreturn [\n msg,\n { payload: logTxt }\n];\n",
"outputs": 2,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 480,
"y": 1540,
"wires": [
[
"4dbdbae84b4d7390"
],
[
"059db6a88f526987",
"cfd34293e830d3cd"
]
]
},
{
"id": "7dbb8d64c879169d",
"type": "inject",
"z": "5fe7408840b1cdf3",
"g": "0ffa85f6b2707ef1",
"name": "22:05",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "05 22 * * *",
"once": false,
"onceDelay": "10",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 220,
"y": 1560,
"wires": [
[
"2b0b581c676e7291"
]
]
},
{
"id": "63fe66bb76b0356c",
"type": "comment",
"z": "5fe7408840b1cdf3",
"g": "0ffa85f6b2707ef1",
"name": "Audit DESS Remy",
"info": "",
"x": 470,
"y": 1360,
"wires": []
},
{
"id": "30fc5ca1af8c2544",
"type": "mqtt out",
"z": "5fe7408840b1cdf3",
"g": "27c9a9a63c937a72",
"name": "Log MQTT->HA",
"topic": "mp2/dess_remy/log_audit",
"qos": "2",
"retain": "true",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "502248144035edbf",
"x": 500,
"y": 1720,
"wires": []
},
{
"id": "4dbdbae84b4d7390",
"type": "debug",
"z": "5fe7408840b1cdf3",
"g": "0ffa85f6b2707ef1",
"name": "debug 102",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "true",
"targetType": "full",
"statusVal": "payload",
"statusType": "auto",
"x": 750,
"y": 1480,
"wires": []
},
{
"id": "059db6a88f526987",
"type": "ui_text",
"z": "5fe7408840b1cdf3",
"g": "0ffa85f6b2707ef1",
"group": "c8a7d12bb7f76f27",
"order": 7,
"width": "12",
"height": "2",
"name": "",
"label": "Log Audit Dess :",
"format": "{{msg.payload}}",
"layout": "row-spread",
"className": "",
"style": false,
"font": "",
"fontSize": 16,
"color": "#000000",
"x": 740,
"y": 1540,
"wires": []
},
{
"id": "2f7797d5a59eb37f",
"type": "link out",
"z": "5fe7408840b1cdf3",
"g": "c64c5ecbe0654837",
"name": "Log Audit",
"mode": "link",
"links": [
"573c180d02c5fc31"
],
"x": 945,
"y": 380,
"wires": []
},
{
"id": "573c180d02c5fc31",
"type": "link in",
"z": "5fe7408840b1cdf3",
"g": "27c9a9a63c937a72",
"name": "Log In",
"links": [
"2f7797d5a59eb37f",
"cfd34293e830d3cd",
"071b658e8fc627e4"
],
"x": 295,
"y": 1720,
"wires": [
[
"30fc5ca1af8c2544"
]
]
},
{
"id": "cfd34293e830d3cd",
"type": "link out",
"z": "5fe7408840b1cdf3",
"g": "0ffa85f6b2707ef1",
"name": "link out 3",
"mode": "link",
"links": [
"573c180d02c5fc31"
],
"x": 675,
"y": 1580,
"wires": []
},
{
"id": "071b658e8fc627e4",
"type": "link out",
"z": "5fe7408840b1cdf3",
"g": "f8afda3620427140",
"name": "link out 4",
"mode": "link",
"links": [
"573c180d02c5fc31"
],
"x": 755,
"y": 1200,
"wires": []
},
{
"id": "8869e559a62cac58",
"type": "change",
"z": "5fe7408840b1cdf3",
"g": "9669903f47819092",
"name": "Horloge",
"rules": [
{
"t": "set",
"p": "horloge",
"pt": "flow",
"to": "payload",
"tot": "msg"
},
{
"t": "set",
"p": "hdebut",
"pt": "flow",
"to": "start",
"tot": "msg"
},
{
"t": "set",
"p": "hfin",
"pt": "flow",
"to": "end",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 340,
"y": 880,
"wires": [
[
"e23bf7ba06f7abf5"
]
]
},
{
"id": "6d096b019c095341",
"type": "function",
"z": "5fe7408840b1cdf3",
"g": "f8afda3620427140",
"name": "H Dynamique V5",
"func": "// =====================================================\n// H_dynamique AMONT BigTimer (gel sur horloge) - Vx finale\n// - Tant que flow.horloge == 0 : recalcul + on_override/off_override\n// - Dès que flow.horloge == 1 : GEL (ne plus recalculer / ne plus envoyer)\n// Sorties :\n// 1) msgOn : \"on_override HH:MM\"\n// 2) msgOff : \"off_override HH:MM\"\n// 3) msgSync : \"sync\"\n// 4) LOG4_DIAG (débridé, rate-limit + changements significatifs)\n// 5) LOG5_EVENT (événementiel, rare)\n// =====================================================\n\n// --- Paramètres ---\nconst CAP_AH = 300; // capacité totale (Ah)\nconst I_AVG = 40; // courant moyen utile (A)\nconst ABS_MIN = 60; // marge absorption/synchro (min)\nconst MIN_WIN = 90; // fenêtre minimale (min)\nconst MAX_WIN = 8 * 60; // garde-fou (min)\nconst STEP = 5; // pas d'arrondi minutes\n\n// Fenêtre imposée (traverse minuit)\nconst START_FLOOR_MIN = 22 * 60 + 0; // 22:00\nconst END_FIXED_MIN = 5 * 60 + 45; // 05:45\n\n// Log4 (diag) : débridé mais borné\nconst LOG4_PERIOD_S = 15 * 60; // 15 min\nconst LOG4_NEED_DELTA_MIN = 10; // changement significatif sur need_min\n\n// --- Utils ---\nfunction clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }\nfunction hhmm(mins) {\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}\nfunction inNightWindow(mins) {\n // Nuit = [22:00..23:59] U [00:00..05:45]\n return (mins >= START_FLOOR_MIN) || (mins <= 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 nightNow = inNightWindow(nowMin);\n\n// --- Entrées flow ---\nlet soc = Number(flow.get('soc'));\nif (!Number.isFinite(soc)) soc = 0;\nsoc = clamp(soc, 0, 100);\n\n// ✅ cible SoC = DESS perso\nlet target = Number(flow.get('dess_soc_cible_pct'));\nif (!Number.isFinite(target)) target = 100;\ntarget = clamp(target, 0, 100);\n\n// ✅ horloge BigTimer\n// 0 = pas en cours, 1 = en cours (fenêtre active)\nconst horloge = Number(flow.get('horloge'));\nconst horlogeOn = (horloge === 1);\n\n// --- LOG5_EVENT helper ---\nfunction mkLog5(txt) {\n // On force le log5 à être uniquement en fenêtre nuit (comme demandé)\n if (!nightNow) return null;\n return { payload: txt };\n}\n\n// =====================================================\n// 0) Détection entrée fenêtre nuit (LOG5_EVENT rare)\n// =====================================================\nconst prevNight = (flow.get('bt_prev_night') === true);\nif (nightNow !== prevNight) flow.set('bt_prev_night', nightNow);\n\nlet msgLog5 = null;\nif (nightNow && !prevNight) {\n msgLog5 = mkLog5(`H_DYN_EVENT @ ${now_hhmm} | entree_fenetre_nuit (22:00-05:45)`);\n}\n\n// =====================================================\n// 1) GEL : si horloge == 1 => on ne calcule plus / on n'envoie plus\n// (on laisse quand même un LOG5 éventuel \"entrée fenêtre\", déjà traité)\n// =====================================================\nif (horlogeOn) {\n node.status({\n fill: \"blue\",\n shape: \"dot\",\n text: `H_dynamique GEL (horloge=1) | SoC=${soc.toFixed(1)}% cible=${target.toFixed(0)}%`\n });\n\n // LOG4 désactivé en GEL, LOG5 seulement si événement entrée fenêtre (déjà construit)\n return [null, null, null, null, msgLog5];\n}\n\n// =====================================================\n// 2) Calcul besoin (autorisé uniquement si horloge==0)\n// =====================================================\nconst delta = Math.max(0, target - soc);\nconst ah_needed = (delta / 100) * CAP_AH;\n\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ébut théorique à partir de la fin ---\nconst start_min_raw = END_FIXED_MIN - need_min;\nlet start_min = start_min_raw;\n\n// Arrondi au pas\nstart_min = Math.floor(start_min / STEP) * STEP;\n\n// Wrap minuit\nlet wrapped = false;\nif (start_min < 0) { start_min += 1440; wrapped = true; }\n\n// --- Appliquer plancher 22:00 (gère minuit) ---\nconst allowed = (start_min >= START_FLOOR_MIN) || (start_min <= END_FIXED_MIN);\nconst clampedTo2200 = (!allowed);\nif (clampedTo2200) start_min = START_FLOOR_MIN;\n\n// --- Fenêtre dispo / ok ---\nconst available_min = (END_FIXED_MIN >= START_FLOOR_MIN)\n ? (END_FIXED_MIN - START_FLOOR_MIN)\n : (1440 - START_FLOOR_MIN + END_FIXED_MIN);\n\nconst ok = (need_min <= available_min);\n\n// --- Heures texte ---\nconst start_hhmm = hhmm(start_min);\nconst end_hhmm = hhmm(END_FIXED_MIN);\nconst start_raw_hhmm = hhmm(start_min_raw);\n\n// --- Expose debug ---\nflow.set('bt_start', start_hhmm);\nflow.set('bt_end', end_hhmm);\nflow.set('bt_need_min', need_min);\nflow.set('bt_available_min', available_min);\nflow.set('bt_ok', ok);\n\n// --- Status Node-RED ---\nnode.status({\n fill: ok ? \"green\" : \"red\",\n shape: ok ? \"dot\" : \"ring\",\n text: ok\n ? `BT ${start_hhmm}→${end_hhmm} | SoC=${soc.toFixed(1)}% cible=${target.toFixed(0)}% need=${need_min}m`\n : `ALERTE need=${need_min}m > dispo=${available_min}m | SoC=${soc.toFixed(1)}% cible=${target.toFixed(0)}%`\n});\n\n// =====================================================\n// 3) LOG4_DIAG (débridé mais rate-limit + changements significatifs)\n// - uniquement dans la fenêtre nuit\n// =====================================================\nlet msgLog4 = null;\nif (nightNow) {\n const nowTs = Math.floor(Date.now() / 1000);\n const lastLog4Ts = Number(flow.get('bt_log4_last_ts'));\n const log4PeriodOk = (!Number.isFinite(lastLog4Ts)) || ((nowTs - lastLog4Ts) >= LOG4_PERIOD_S);\n\n const lastNeed = Number(flow.get('bt_last_need_min'));\n const lastOk = (flow.get('bt_last_ok') === true);\n\n const needChanged = Number.isFinite(lastNeed) ? (Math.abs(need_min - lastNeed) >= LOG4_NEED_DELTA_MIN) : true;\n const okChanged = (ok !== lastOk);\n\n // comparaison start envoyé (pour savoir si on est stable)\n const lastStartSent = (flow.get('bt_last_start_sent') || '').toString();\n const changedStart = (start_hhmm !== lastStartSent);\n\n const shouldLog4 = (log4PeriodOk || needChanged || okChanged || changedStart);\n\n if (shouldLog4) {\n flow.set('bt_log4_last_ts', nowTs);\n flow.set('bt_last_need_min', need_min);\n flow.set('bt_last_ok', ok);\n\n const flags = [wrapped ? \"wrap\" : null, clampedTo2200 ? \"clamp22\" : null].filter(Boolean).join(\",\");\n\n const logTxt =\n `H_DYN_DIAG @ ${now_hhmm}` +\n ` | horloge=${horloge}` +\n ` | SoC=${soc.toFixed(1)}% -> cible=${target.toFixed(0)}% (Δ=${delta.toFixed(1)}%)` +\n ` | start=${start_hhmm} end=${end_hhmm}` +\n ` | need=${need_min}m (bulk=${Math.ceil(bulk_min)}m abs=${abs_min}m)` +\n ` | dispo=${available_min}m ok=${ok}` +\n ` | lastStartSent=${lastStartSent || \"-\"}` +\n (flags ? ` | flags=${flags}` : \"\");\n\n msgLog4 = { payload: logTxt };\n }\n}\n\n// =====================================================\n// 4) Envoi overrides BigTimer (seulement si changement d'heure de début)\n// + LOG5_EVENT sur override envoyé / alerte\n// =====================================================\nconst lastStartSent = (flow.get('bt_last_start_sent') || '').toString();\nconst changed = (start_hhmm !== lastStartSent);\n\n// LOG5_EVENT si alerte (ok=false) : rare mais utile\nif (!ok && nightNow) {\n msgLog5 = mkLog5(\n `H_DYN_EVENT @ ${now_hhmm} | ALERTE need=${need_min}m > dispo=${available_min}m | SoC=${soc.toFixed(1)}% cible=${target.toFixed(0)}%`\n );\n}\n\n//if (!changed) {\n // Pas d’override, mais on peut sortir LOG4 (si présent) + LOG5 (si présent)\n// return [null, null, null, msgLog4, msgLog5];\n//}\n\n// Overrides si changement\nconst msgOn = { payload: `on_override ${start_hhmm}` };\nconst msgOff = { payload: `off_override ${end_hhmm}` };\nconst msgSync = { payload: \"sync\" };\n\n// Mémorise l’override envoyé\nflow.set('bt_last_start_sent', start_hhmm);\nflow.set('bt_last_sent_ts', now.toISOString());\n\n// LOG5_EVENT : override envoyé (événement clé)\nif (nightNow) {\n const flags = [wrapped ? \"wrap\" : null, clampedTo2200 ? \"clamp22\" : null].filter(Boolean).join(\",\");\n msgLog5 = mkLog5(\n `H_DYN_EVENT @ ${now_hhmm} | OVERRIDE sent start=${start_hhmm} end=${end_hhmm} | SoC=${soc.toFixed(1)}% cible=${target.toFixed(0)}% need=${need_min}m` +\n (flags ? ` | flags=${flags}` : \"\")\n );\n}\n\nreturn [msgOn, msgOff, msgSync, msgLog4, msgLog5];\n",
"outputs": 5,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 390,
"y": 1140,
"wires": [
[
"665a65c2ac7c84ce"
],
[
"665a65c2ac7c84ce"
],
[
"233a80856735ebfb"
],
[
"701744df73f35cf2"
],
[
"071b658e8fc627e4"
]
]
},
{
"id": "233a80856735ebfb",
"type": "delay",
"z": "5fe7408840b1cdf3",
"g": "f8afda3620427140",
"name": "",
"pauseType": "delay",
"timeout": "200",
"timeoutUnits": "milliseconds",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": false,
"outputs": 1,
"x": 690,
"y": 1120,
"wires": [
[
"665a65c2ac7c84ce"
]
]
},
{
"id": "56c0dd1a9c9f59d5",
"type": "comment",
"z": "5fe7408840b1cdf3",
"g": "27c9a9a63c937a72",
"name": "Log Vers MQTT pour Stockage ds fichier via Home Assistant",
"info": "",
"x": 440,
"y": 1680,
"wires": []
},
{
"id": "c3ef0fbdd807d8bc",
"type": "victron-input-ess",
"z": "5fe7408840b1cdf3",
"g": "f7b3a04d7293fa0c",
"service": "com.victronenergy.settings",
"path": "/Settings/CGwacs/BatteryLife/Schedule/Charge/1/Day",
"serviceObj": {
"service": "com.victronenergy.settings",
"name": "Venus settings"
},
"pathObj": {
"path": "/Settings/CGwacs/BatteryLife/Schedule/Charge/1/Day",
"type": "enum",
"name": "Schedule 2: Day",
"enum": {
"0": "Sunday",
"1": "Monday",
"2": "Tuesday",
"3": "Wednesday",
"4": "Thursday",
"5": "Friday",
"6": "Saturday",
"7": "Every day",
"8": "Weekdays",
"9": "Weekends",
"11": "Monthly",
"-1": "Disabled"
},
"remarks": "<p>A negative value means that the schedule has been de-activated.</p>",
"mode": "both"
},
"name": "",
"onlyChanges": false,
"x": 210,
"y": 1820,
"wires": [
[
"a45f19bbc80e5a79"
]
]
},
{
"id": "a45f19bbc80e5a79",
"type": "function",
"z": "5fe7408840b1cdf3",
"g": "f7b3a04d7293fa0c",
"name": "function 22",
"func": "// Entrée: msg.payload = valeur de /Settings/CGwacs/BatteryLife/Schedule/Charge/0/Day\nconst day = Number(msg.payload);\n\nlet status;\nlet enabled = false;\n\nif (!Number.isFinite(day)) {\n status = \"Day invalide\";\n} else if (day === -1) {\n status = \"Desactive\";\n} else if (day === -7) {\n status = \"Indefini\";\n} else if (day >= 0) {\n enabled = true;\n status = `Actif (Day=${day})`;\n} else {\n status = `Inconnu (Day=${day})`;\n}\n\n// Sorties utiles\nmsg.day = day;\nmsg.enabled = enabled; // booléen équivalent du switch *_enabled\nmsg.status_text = status;\n\n// Si vous voulez payload = bool\nmsg.payload = enabled;\n\n// Optionnel: statut Node-RED\nnode.status({ fill: enabled ? \"green\" : \"grey\", shape: \"dot\", text: status });\n\nreturn msg;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 510,
"y": 1820,
"wires": [
[
"fda0c5fc9d3a3778"
]
]
},
{
"id": "fda0c5fc9d3a3778",
"type": "debug",
"z": "5fe7408840b1cdf3",
"g": "f7b3a04d7293fa0c",
"name": "debug 103",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "true",
"targetType": "full",
"statusVal": "payload",
"statusType": "auto",
"x": 670,
"y": 1820,
"wires": []
},
{
"id": "2b5dea20fe8df2e3",
"type": "victron-output-ess",
"z": "5fe7408840b1cdf3",
"g": "f7b3a04d7293fa0c",
"service": "com.victronenergy.settings",
"path": "/Settings/CGwacs/BatteryLife/Schedule/Charge/1/Day",
"serviceObj": {
"service": "com.victronenergy.settings",
"name": "Venus settings"
},
"pathObj": {
"path": "/Settings/CGwacs/BatteryLife/Schedule/Charge/1/Day",
"type": "enum",
"name": "Schedule 2: Day",
"enum": {
"0": "Sunday",
"1": "Monday",
"2": "Tuesday",
"3": "Wednesday",
"4": "Thursday",
"5": "Friday",
"6": "Saturday",
"7": "Every day",
"8": "Weekdays",
"9": "Weekends",
"11": "Monthly",
"-1": "Disabled"
},
"remarks": "<p>A negative value means that the schedule has been de-activated.</p>",
"mode": "both"
},
"name": "Validation CP2",
"onlyChanges": false,
"x": 680,
"y": 1920,
"wires": []
},
{
"id": "e95a6ca3e8f41607",
"type": "inject",
"z": "5fe7408840b1cdf3",
"g": "f7b3a04d7293fa0c",
"name": "7",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "7",
"payloadType": "num",
"x": 350,
"y": 1900,
"wires": [
[
"2b5dea20fe8df2e3"
]
]
},
{
"id": "1cd50a3b5905c265",
"type": "inject",
"z": "5fe7408840b1cdf3",
"g": "f7b3a04d7293fa0c",
"name": "-1",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "-1",
"payloadType": "num",
"x": 350,
"y": 1960,
"wires": [
[
"2b5dea20fe8df2e3"
]
]
},
{
"id": "3771889792a45c9c",
"type": "mqtt out",
"z": "5fe7408840b1cdf3",
"g": "c64c5ecbe0654837",
"name": "Previ Conso J+1",
"topic": "mp2/dess_remy/previ_conso_6-22-J1",
"qos": "",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "502248144035edbf",
"x": 560,
"y": 400,
"wires": []
},
{
"id": "58133dfe4f341acc",
"type": "comment",
"z": "5fe7408840b1cdf3",
"g": "f8afda3620427140",
"name": "Calcul Heure de début activtion de la Charge Programée 1 de ESS Victron",
"info": "",
"x": 580,
"y": 1060,
"wires": []
},
{
"id": "502248144035edbf",
"type": "mqtt-broker",
"name": "MQTTHA",
"broker": "192.168.0.37",
"port": "1883",
"clientid": "",
"autoConnect": true,
"usetls": false,
"protocolVersion": "4",
"keepalive": "60",
"cleansession": true,
"autoUnsubscribe": true,
"birthTopic": "",
"birthQos": "0",
"birthPayload": "",
"birthMsg": {},
"closeTopic": "",
"closeQos": "0",
"closePayload": "",
"closeMsg": {},
"willTopic": "",
"willQos": "0",
"willPayload": "",
"willMsg": {},
"userProps": "",
"sessionExpiry": ""
},
{
"id": "c8a7d12bb7f76f27",
"type": "ui_group",
"name": "DESS",
"tab": "8c1c69b7bf39eb5b",
"order": 1,
"disp": true,
"width": "12",
"collapse": false,
"className": ""
},
{
"id": "5e3a89e5eaeb1cb4",
"type": "config-vrm-api",
"name": "vrm_remy"
},
{
"id": "8c1c69b7bf39eb5b",
"type": "ui_tab",
"name": "MP2",
"icon": "dashboard",
"disabled": false,
"hidden": false
}
]
Mini Table des topics utilisé dans mqtt
HA → Node-RED (commandes / forçages)
ha/mp2/cp/validcp(autorisation globale CP)ha/mp2/cp/forcage100(forçage manuel ON/OFF)ha/mp2/cp/niveauforcagecp1(niveau SoC cible forcé)
Node-RED → HA (supervision)
mp2/dess_remy/prod_total(prévision PV J+1 6–22, QoS 2 + retain)mp2/dess_remy/cible_soc(SoC cible retenu)mp2/multiplus2/valide_cp(état CP :on/off)mp2/dess_remy/h_debut(heure début CP,HH:MM)mp2/dess_remy/duree(durée CP,HH:MM)
