#!/usr/bin/env node // P8 v2 (07/08/2026) : libération du réseau HC quand le véhicule est plein — version COLLANTE. // // Problème du signal naïf : le VehicleDemandFilter retombe à false dès que le relais // s'ouvre (session désarmée) → un seul cycle de release puis redémarrage en boucle. // Solution : mémoire auto-entretenue via l'état surplus relu au cycle suivant : // charge → VDF détecte voiture pleine → state 'vehicle_full_or_paused' // cycle N+1 : WINDOWS voit ce state → vehicleChargeCompleted → DECISION émet // 'night_session_done_idle' → cycles suivants : ce state s'auto-entretient // jusqu'à débranchement (state vehicle_not_connected) ou fin de fenêtre. // // Effets : // - latch takeover libéré (fix de l'inversion du 04/08) // - préférence HC (P7) relâchée // - sourceMode nuit → 'blocked' (plus aucune tentative de charge) // - takeover helper OFF si SOC > cible matin → l'hystérésis coupe EDF // - takeover helper ON si SOC ≤ cible → EDF maintenu + ACM recharge la batterie // (le nom 'night_session_done_idle' ne matche AUCUN pattern no-demand de // l'hystérésis, donc l'override batterie-basse reste fonctionnel) import fs from 'fs'; const FILE = 'workflows/evcs-surplus-controller-v2.6.json'; const wf = JSON.parse(fs.readFileSync(FILE, 'utf8')); function getNode(name) { const n = wf.nodes.find(x => x.name === name); if (!n) throw new Error(`Node introuvable: ${name}`); return n; } function replaceExact(code, from, to, label) { if (!code.includes(from)) throw new Error(`Pattern introuvable (${label})`); if (code.split(from).length !== 2) throw new Error(`Pattern non unique (${label})`); return code.replace(from, to); } // ===================================================== // A. SNAPSHOT TEMPLATE : relire son propre état surplus // ===================================================== const snap = getNode('HA Snapshot EVCS Surplus Controller V2.2'); const tplParam = snap.parameters.bodyParameters.parameters.find(p => p.name === 'template'); tplParam.value = replaceExact( tplParam.value, `"night_takeover_prev": states("input_boolean.victron_evcs_night_grid_takeover_needed"),`, `"night_takeover_prev": states("input_boolean.victron_evcs_night_grid_takeover_needed"), "prev_surplus_state": states("input_text.victron_evcs_surplus_state"),`, 'A template prev_surplus_state' ); console.log('OK: template snapshot (+prev_surplus_state)'); // ===================================================== // B. WINDOWS_SOURCEV2 // ===================================================== const wNode = getNode('WINDOWS_SOURCEV2'); let w = wNode.parameters.jsCode; // B1. vehicleChargeCompleted collant + fix latch inversé (bornes par index : // le bloc 04/08 contient des espaces traînants) const startAnchor = 'const vehicleChargeCompleted ='; const endAnchor = 'const nightGridHcPreferred ='; const s = w.indexOf(startAnchor), e = w.indexOf(endAnchor); if (s === -1 || e === -1 || e < s) throw new Error('B1: bornes introuvables'); w = w.slice(0, s) + `// P8 (07/08/2026) : "charge terminée" COLLANT via l'état surplus du cycle // précédent (helper input_text.victron_evcs_surplus_state relu par le snapshot). // Chaîne : VDF stoppe la charge (state vehicle_full_or_paused) → ce flag s'allume // → DECISION émet night_session_done_idle → le flag s'auto-entretient jusqu'au // débranchement (state vehicle_not_connected) ou à la fin de la fenêtre nuit. const prevSurplusState = (input.prev_surplus_state ?? '').toString().trim().toLowerCase(); const vehicleChargeCompleted = nightSessionActive && ( prevSurplusState === 'vehicle_full_or_paused' || prevSurplusState === 'night_session_done_idle' ); // P8 : correction du fix 04/08 (logique inversée : "|| vehicleChargeCompleted" // GARDAIT le latch après la charge au lieu de le libérer). const nightTakeoverLatched = nightSessionActive && nightTakeoverPrev && parsed.soc < NIGHT_TAKEOVER_RELEASE_SOC_PCT && !vehicleChargeCompleted; ` + w.slice(e); // B2. La préférence HC (P7) tombe quand la charge est terminée w = replaceExact(w, `const nightGridHcPreferred = nightSessionActive && params.allowGridHcNight;`, `const nightGridHcPreferred = nightSessionActive && params.allowGridHcNight && !vehicleChargeCompleted;`, 'B2 préférence P7' ); // B3. sourceMode nuit : véhicule plein → 'blocked' (aucune tentative de charge) w = replaceExact(w, `const nightPreferredSourceMode = !nightSessionActive ? 'blocked' : batteryAssistAllowed ? 'battery_assist_night'`, `const nightPreferredSourceMode = !nightSessionActive ? 'blocked' : vehicleChargeCompleted ? 'blocked' : batteryAssistAllowed ? 'battery_assist_night'`, 'B3 preferred blocked' ); // B4. Export du flag dans le preview (consommé par BCL → DECISION) w = replaceExact(w, ` batteryAssistAllowed, nightGridTakeoverNeeded, nightTakeoverLatched, nightGridHcPreferred, nightPreferredSourceMode, }`, ` batteryAssistAllowed, nightGridTakeoverNeeded, nightTakeoverLatched, nightGridHcPreferred, vehicleChargeCompleted, nightPreferredSourceMode, }`, 'B4 export preview' ); wNode.parameters.jsCode = w; console.log('OK: WINDOWS_SOURCEV2 (B1-B4)'); // ===================================================== // C. BATTERY_CLOUD_LOCKS : passer le flag dans nightEvcs // ===================================================== const bNode = getNode('BATTERY_CLOUD_LOCKS'); let b = bNode.parameters.jsCode; b = replaceExact(b, `const nightTakeoverLatched = !!nightPrev.nightTakeoverLatched;`, `const nightTakeoverLatched = !!nightPrev.nightTakeoverLatched; const vehicleChargeCompleted = !!nightPrev.vehicleChargeCompleted;`, 'C1 consume' ); b = replaceExact(b, ` batteryAssistAllowed, nightGridTakeoverNeeded, nightTakeoverLatched, nightPreferredSourceMode, },`, ` batteryAssistAllowed, nightGridTakeoverNeeded, nightTakeoverLatched, vehicleChargeCompleted, nightPreferredSourceMode, },`, 'C2 export nightEvcs' ); bNode.parameters.jsCode = b; console.log('OK: BATTERY_CLOUD_LOCKS (C1-C2)'); // ===================================================== // D. DECISION_ENGINEV2 : état auto-entretenu night_session_done_idle // (⚠️ le nom ne doit matcher AUCUN pattern no-demand de l'hystérésis : // full_or_paused / not_requesting_power / charge_complete / no_demand) // ===================================================== const dNode = getNode('DECISION_ENGINEV2'); let d = dNode.parameters.jsCode; d = replaceExact(d, ` else if (context.sourceMode === 'blocked') { setOff({ stateName: context.profile === 'outside_window' ? 'blocked_outside_window'`, ` // P8 : véhicule plein en session nuit → veille auto-entretenue. // Le state est relu au cycle suivant (prev_surplus_state) : il maintient // vehicleChargeCompleted jusqu'au débranchement ou à la fin de fenêtre. else if (context.profile === 'night_hc' && nightEvcs?.vehicleChargeCompleted) { setOff({ stateName: 'night_session_done_idle', decisionName: 'hold', reasonText: 'Charge véhicule terminée : veille nuit, réseau relâché (recharge batterie si sous cible)', dynamic: false, mustStopFlag: false, blockReason: 'none', blockedUntilStamp: now.stamp, }); } else if (context.sourceMode === 'blocked') { setOff({ stateName: context.profile === 'outside_window' ? 'blocked_outside_window'`, 'D1 branche done_idle' ); dNode.parameters.jsCode = d; console.log('OK: DECISION_ENGINEV2 (D1)'); fs.writeFileSync(FILE, JSON.stringify(wf, null, 2) + '\n'); console.log('Écrit:', FILE);