#!/usr/bin/env node // R1 (07/08/2026) : déduplication du moteur nuit EVCS. // - WINDOWS_SOURCEV2 devient l'UNIQUE calculateur (ajoute les 4 champs que seul // BCL calculait : nightSessionStartMs, needsCapture strict, batteryRecoveryAllowedNow, // batteryTargetReached) et exporte tout dans nightEvcsPreview. // - BATTERY_CLOUD_LOCKS ne recalcule plus rien : il consomme prev.nightEvcsPreview. // Le shape de l'export nightEvcs de BCL (consommé par DECISION/HA_SERVICE) est inchangé. 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 node = wf.nodes.find(n => n.name === name); if (!node) throw new Error(`Node introuvable: ${name}`); return node; } function replaceExact(code, from, to, label) { if (!code.includes(from)) throw new Error(`Pattern introuvable (${label}):\n---\n${from}\n---`); if (code.split(from).length !== 2) throw new Error(`Pattern non unique (${label})`); return code.replace(from, to); } // ===================================================== // 1. WINDOWS_SOURCEV2 // ===================================================== const wNode = getNode('WINDOWS_SOURCEV2'); let w = wNode.parameters.jsCode; // 1a. nightSessionStartMs (repris de BCL — bookkeeping de la mémoire de session) w = replaceExact( w, `const nightSessionStartAt = nightSessionActive ? (history.nightSessionStartAt || '') : '';`, `const nightSessionStartAt = nightSessionActive ? (history.nightSessionStartAt || '') : ''; const nightSessionStartMs = nightSessionActive ? (history.nightSessionStartMs ?? null) : null;`, 'W-1a startMs' ); // 1b. needsCapture : version stricte (celle de BCL, qui faisait foi en prod // via DECISION) — inclut le critère nightSessionStartMs. w = replaceExact( w, `const nightSessionNeedsCapture = nightSessionActive && ( !Number.isFinite(history.nightSessionStartSocPct) || !history.nightSessionStartAt );`, `const nightSessionNeedsCapture = nightSessionActive && ( !Number.isFinite(history.nightSessionStartSocPct) || !nightSessionStartAt || nightSessionStartMs === null );`, 'W-1b needsCapture strict' ); // 1c. batteryRecoveryAllowedNow + batteryTargetReached (repris de BCL) w = replaceExact( w, `const nightPreferredSourceMode = !nightSessionActive ? 'blocked' : batteryAssistAllowed ? 'battery_assist_night' : ( batteryBelowHardFloor || batteryBelowMorningTarget || batteryRecoveryUrgent ) ? 'grid_forced' : 'grid_hc_allowed';`, `const nightPreferredSourceMode = !nightSessionActive ? 'blocked' : batteryAssistAllowed ? 'battery_assist_night' : ( batteryBelowHardFloor || batteryBelowMorningTarget || batteryRecoveryUrgent ) ? 'grid_forced' : 'grid_hc_allowed'; const batteryRecoveryAllowedNow = nightSessionActive && nightGridTakeoverNeeded && ( batteryBelowMorningTarget || batteryRecoveryUrgent ); const batteryTargetReached = nightSessionActive && parsed.soc >= batteryMorningTargetPct;`, 'W-1c recoveryAllowedNow + targetReached' ); // 1d. Export complet (nightEvcsPreview ⊇ nightEvcs de BCL + champs debug) w = replaceExact( w, ` nightEvcsPreview: { nightSessionActive, nightSessionNeedsCapture, nightSessionNeedsClear, nightSessionStartSocPct, nightSessionStartAt, batteryHardFloorPct, batteryMorningTargetPct, maxBatteryDischargePoints, gridTakeoverTriggerPct, batteryBelowHardFloor, batteryBelowMorningTarget, batteryAtOrBelowGridTakeoverTrigger, batteryKwhMissingToTarget, batteryRecoveryMinutesNeeded, batteryRecoveryUrgent, batteryAssistAllowed, nightGridTakeoverNeeded, nightPreferredSourceMode, }`, ` // R1 : source de vérité UNIQUE du moteur nuit. // BATTERY_CLOUD_LOCKS consomme cet objet, il ne recalcule plus rien. nightEvcsPreview: { nightSessionActive, nightSessionNeedsCapture, nightSessionNeedsClear, nightSessionStartSocPct, nightSessionStartAt, nightSessionStartMs, batteryHardFloorPct, batteryMorningTargetPct, maxBatteryDischargePoints, gridTakeoverTriggerPct, batteryBelowHardFloor, batteryBelowMorningTarget, batteryAtOrBelowGridTakeoverTrigger, batteryKwhMissingToTarget, batteryRecoveryMinutesNeeded, batteryRecoveryUrgent, batteryRecoveryAllowedNow, batteryTargetReached, batteryAssistAllowed, nightGridTakeoverNeeded, nightTakeoverLatched, nightGridHcPreferred, nightPreferredSourceMode, }`, 'W-1d export étendu' ); wNode.parameters.jsCode = w; console.log('OK: WINDOWS_SOURCEV2 (4 modifications)'); // ===================================================== // 2. BATTERY_CLOUD_LOCKS : remplacement du bloc dupliqué // ===================================================== const bNode = getNode('BATTERY_CLOUD_LOCKS'); let b = bNode.parameters.jsCode; const START_MARKER = `// ===================================================== // EVCS NIGHT ENGINE // =====================================================`; const END_MARKER = `const batteryTargetReached = nightSessionActive && parsed.soc >= batteryMorningTargetPct;`; const startIdx = b.indexOf(START_MARKER); const endIdx = b.indexOf(END_MARKER); if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) { throw new Error('Bornes du bloc EVCS NIGHT ENGINE introuvables dans BCL'); } const removed = b.slice(startIdx, endIdx + END_MARKER.length); console.log(`BCL: bloc dupliqué localisé (${removed.split('\n').length} lignes, ${removed.length} chars)`); const CONSUME_BLOCK = `// ===================================================== // EVCS NIGHT ENGINE — R1 (07/08/2026) // Le moteur nuit est calculé UNE SEULE FOIS dans WINDOWS_SOURCEV2 // (payload nightEvcsPreview). Ce node CONSOMME, il ne recalcule plus. // TOUTE évolution de la logique nuit se fait dans WINDOWS_SOURCEV2. // Fallback sûr si preview absent : session inactive / 'blocked'. // ===================================================== const nightPrev = prev.nightEvcsPreview || {}; const nightSessionActive = !!nightPrev.nightSessionActive; const nightSessionNeedsCapture = !!nightPrev.nightSessionNeedsCapture; const nightSessionNeedsClear = !!nightPrev.nightSessionNeedsClear; const nightSessionStartSocPct = Number.isFinite(nightPrev.nightSessionStartSocPct) ? nightPrev.nightSessionStartSocPct : null; const nightSessionStartAt = nightPrev.nightSessionStartAt || ''; const nightSessionStartMs = Number.isFinite(nightPrev.nightSessionStartMs) ? nightPrev.nightSessionStartMs : null; const batteryHardFloorPct = Number.isFinite(nightPrev.batteryHardFloorPct) ? nightPrev.batteryHardFloorPct : params.evcsNightBatteryHardFloorPct; const batteryMorningTargetPct = Number.isFinite(nightPrev.batteryMorningTargetPct) ? nightPrev.batteryMorningTargetPct : params.evcsNightTargetWeakForecastPct; const maxBatteryDischargePoints = Number.isFinite(nightPrev.maxBatteryDischargePoints) ? nightPrev.maxBatteryDischargePoints : 0; const gridTakeoverTriggerPct = Number.isFinite(nightPrev.gridTakeoverTriggerPct) ? nightPrev.gridTakeoverTriggerPct : Math.max(batteryHardFloorPct, batteryMorningTargetPct); const batteryBelowHardFloor = !!nightPrev.batteryBelowHardFloor; const batteryBelowMorningTarget = !!nightPrev.batteryBelowMorningTarget; const batteryAtOrBelowGridTakeoverTrigger = !!nightPrev.batteryAtOrBelowGridTakeoverTrigger; const batteryKwhMissingToTarget = Number.isFinite(nightPrev.batteryKwhMissingToTarget) ? nightPrev.batteryKwhMissingToTarget : 0; const batteryRecoveryMinutesNeeded = Number.isFinite(nightPrev.batteryRecoveryMinutesNeeded) ? nightPrev.batteryRecoveryMinutesNeeded : 0; const batteryRecoveryUrgent = !!nightPrev.batteryRecoveryUrgent; const batteryRecoveryAllowedNow = !!nightPrev.batteryRecoveryAllowedNow; const batteryTargetReached = !!nightPrev.batteryTargetReached; const batteryAssistAllowed = !!nightPrev.batteryAssistAllowed; const nightGridTakeoverNeeded = !!nightPrev.nightGridTakeoverNeeded; const nightTakeoverLatched = !!nightPrev.nightTakeoverLatched; const nightPreferredSourceMode = nightPrev.nightPreferredSourceMode || 'blocked';`; b = b.slice(0, startIdx) + CONSUME_BLOCK + b.slice(endIdx + END_MARKER.length); bNode.parameters.jsCode = b; console.log('OK: BATTERY_CLOUD_LOCKS (bloc dupliqué remplacé par la consommation du preview)'); fs.writeFileSync(FILE, JSON.stringify(wf, null, 2) + '\n'); console.log(`Écrit: ${FILE}`);