#!/usr/bin/env node // Patch 09/08/2026 — FIX E2 (relais) : coupure EDF immédiate quand l'état // surplus est 'night_recovery_done_idle' (collant : le takeover ne peut plus // revenir) et qu'aucun override n'est actif. Évite les 25 min de // hold_on_min_runtime après la tentative de charge d'un véhicule déjà plein. // 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_recovery_done')) { console.log('Déjà appliqué — rien à faire.'); process.exit(0); } const anchor = ` state = 'turn_off_vehicle_unplugged'; reason.push('Vehicule debranche + SOC >= seuil OFF : coupure immediate du reseau (bypass min_on)'); forcedCut = true; } `; const insertion = ` state = 'turn_off_vehicle_unplugged'; reason.push('Vehicule debranche + SOC >= seuil OFF : coupure immediate du reseau (bypass min_on)'); forcedCut = true; } // ====================================================== // FIX E2 (09/08/2026): RECOVERY DONE -> COUPURE IMMEDIATE // 'night_recovery_done_idle' est un etat COLLANT du controleur EVCS : // charge terminee ET batterie a la cible, le takeover ne reviendra pas // (sauf SOC <= cible-2, qui repasserait par l'override). Attendre le // min_on 25 min ne protege ici de rien : coupure immediate. // ====================================================== if ( !forcedCut && latchIsOn && gridIsOn && !evcsGridOverride && evcsSurplusState === 'night_recovery_done_idle' && soc >= gridSocOff ) { desiredGrid = 'off'; desiredLatch = 'off'; 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 turn_off_vehicle_unplugged introuvable — appliquer d abord patch-20260808-c-unplug-cut.mjs'); code = code.replace(anchor, insertion); const notifAnchor = `} else if (state === 'turn_off_recovered') {`; const notifInsertion = `} else if (state === 'turn_off_recovery_done') { notifyTitle = '✅ EDF coupé (charge terminée)'; notifyMessage = \`Charge VE terminée et batterie à la cible (SOC \${round1(soc)}%). Coupure immédiate du réseau.\`; notifyWanted = true; } else if (state === 'turn_off_recovered') {`; if (!code.includes(notifAnchor)) throw new Error('Ancre notification turn_off_recovered introuvable'); code = code.replace(notifAnchor, notifInsertion); node.parameters.jsCode = code; fs.writeFileSync(FILE, JSON.stringify(wf, null, 2) + '\n'); console.log('Patch appliqué →', FILE, `(jsCode ${code.length} chars)`);