#!/usr/bin/env node // Patch 22/08/2026 — FIX G (relais) : coupure EDF immédiate, générique, dès // que l'override EVCS disparaît (evcsGridOverride === false) et que le SOC // est au-dessus du seuil bas ON (gridSocOn, PAS gridSocOff) — au lieu de // dépendre d'une liste blanche de noms exacts de surplus_state (Fix C/E2), // qui ratait les états transitoires génériques (stop_source_lost, // idle_wait_source) émis quand la fenêtre HC nuit se ferme mi-session. // Incident : nuit 21-22/08/2026, EDF resté ON des heures après la fermeture // de la fenêtre HC, PV plafonné ~100W par le MPPT. // Cible : workflows/victron-grid-hysteresis-v1.json (node "Compute Grid Hysteresis"). // Rejouable : détecte si déjà appliqué. import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const FILE = path.join(ROOT, 'workflows/victron-grid-hysteresis-v1.json'); const wf = JSON.parse(fs.readFileSync(FILE, 'utf8')); const node = wf.nodes.find(n => n.name === 'Compute Grid Hysteresis'); if (!node) throw new Error('Node "Compute Grid Hysteresis" introuvable'); let code = node.parameters.jsCode; if (code.includes('turn_off_evcs_override_released')) { console.log('Déjà appliqué — rien à faire.'); process.exit(0); } const anchor = ` state = 'turn_off_recovery_done'; reason.push('Charge terminee + batterie a la cible : coupure immediate du reseau (bypass min_on)'); forcedCut = true; } `; if (!code.includes(anchor)) { throw new Error('Ancre Fix E2 introuvable — le node a peut-être changé, vérifier manuellement.'); } const insertion = `${anchor} // ====================================================== // FIX G (22/08/2026): OVERRIDE EVCS DISPARU -> COUPURE IMMEDIATE // Incident nuit 21-22/08 : a la fermeture de la fenetre HC (~06-07h), le // controleur EVCS remet correctement son override a false MAIS le vehicule // n'est pas encore flag "charge terminee" -> DECISION_ENGINEV2 retombe sur // des etats generiques transitoires (stop_source_lost, idle_wait_source, // et potentiellement d'autres non enumeres ici). Fix C/E2 ne matchent // aucun de ces noms -> EDF restait maintenu ON par la boucle generique // jusqu'a gridSocOff (35%), potentiellement des heures. // // Regle GENERIQUE (pas un whitelist de plus) : des que evcsGridOverride // redevient false ET SOC > gridSocOn (22%, PAS gridSocOff 35% : on ne // touche pas au cas independant "SOC bas" qui doit tenir jusqu'a // gridSocOff), on coupe immediatement. // // Garde anti-glitch : n'agit pas si la lecture evcs_source_mode est // vide/unavailable/unknown (panne HA transitoire) - dans ce cas la boucle // generique gere normalement plutot que de couper sur une lecture non fiable. // ====================================================== const evcsSourceModeLooksValid = evcsSourceModeRaw !== '' && !['unavailable', 'unknown'].includes(low(evcsSourceModeRaw)); if ( !forcedCut && latchIsOn && gridIsOn && !evcsGridOverride && soc > gridSocOn && evcsSourceModeLooksValid ) { desiredGrid = 'off'; desiredLatch = 'off'; state = 'turn_off_evcs_override_released'; reason.push(\`Override EVCS disparu (source=\${evcsSourceModeRaw || 'n/a'}, surplus_state=\${evcsSurplusStateRaw || 'n/a'}) + SOC \${round1(soc)}% > seuil ON \${gridSocOn}% : coupure immediate du reseau (bypass min_on, libere le solaire)\`); forcedCut = true; } `; code = code.replace(anchor, insertion); const notifAnchor = `} else if (state === 'turn_off_recovered') {`; if (!code.includes(notifAnchor)) { throw new Error('Ancre notification turn_off_recovered introuvable.'); } const notifInsertion = `} else if (state === 'turn_off_evcs_override_released') { notifyTitle = '✅ EDF coupé (override EVCS disparu)'; notifyMessage = \`L'override EVCS a disparu (source=\${evcsSourceModeRaw || 'n/a'}) et SOC \${round1(soc)}% > seuil ON \${gridSocOn}%. Coupure immédiate du réseau pour libérer le solaire.\`; notifyWanted = true; ${notifAnchor}`; code = code.replace(notifAnchor, notifInsertion); node.parameters.jsCode = code; fs.writeFileSync(FILE, JSON.stringify(wf, null, 2) + '\n', 'utf8'); console.log('Fix G appliqué à', FILE);