#!/usr/bin/env node // Dry-run du patch nightGridHcPreferred : exécute le jsCode réel de WINDOWS_SOURCEV2 // avec un snapshot simulé (nuit HC, véhicule branché) et affiche la décision nuit. import fs from 'fs'; import vm from 'vm'; const wf = JSON.parse(fs.readFileSync('workflows/evcs-surplus-controller-v2.6.json', 'utf8')); const windowsCode = wf.nodes.find(n => n.name === 'WINDOWS_SOURCEV2').parameters.jsCode; function buildPrev({ soc, allowGridHcNight, vehicleConnected, minNow, hour, batteryCurrentA, takeoverPrev, gridAvailable }) { return { input: { night_takeover_prev: takeoverPrev ? 'on' : 'off' }, nowMs: Date.now(), now: { minNow, hour, minute: minNow % 60, stamp: '2026-08-07 01:30:00', date: '2026-08-07' }, cfg: { batteryUsableCapacityKwh: 17.5, nominalAcVoltageV: 230, systemReserveW: 300, evcsDedicatedMaxW: 4900, vebusDcVoltageHardFloorV: 47, vebusDcVoltageSoftFloorV: 48, }, parsed: { soc, batteryCurrentA, pvPowerW: 0, pvPowerAvg10mW: 0, pvTrendW: 0, houseLoadW: 700, houseAcPowerW: 700, houseAcPowerPrevW: 700, evcsAcPowerW: 0, vebusAcOutPowerW: 700, vebusAcOutVoltageV: 230, acInAvailable: false, evcsPowerW: 0, evcsVehicleConnected: vehicleConnected, evcsDeviceConnected: true, evcsChargingSwitchOn: false, evcsModeIsAuto: false, controlLatch: false, gridAvailable: gridAvailable ?? false, acVoltageV: 230, batteryVoltageV: 51, vebusAcOutW: 700, vebusDcVoltageV: 51, todayRemainingKwh: 0, tomorrowKwh: 25, lastStartAt: '', lastStopAt: '', lastCurrentChangeAt: '', probeStartedAt: '', departureTime: '08:50:00', departurePriority: true, requestedEnergyKwh: 10.5, currentSetA: 6, }, params: { userStrategyRaw: 'auto_solar_plus', allowGridHcNight, allowGridHcMidday: true, hcNightStartMin: 1 * 60 + 24, hcNightStopMin: 7 * 60 + 24, morningStartMin: 7 * 60 + 30, morningStopMin: 11 * 60, hcMiddayStartMin: 12 * 60 + 24, hcMiddayStopMin: 14 * 60 + 24, eveningStartMin: 17 * 60, eveningStopMin: 20 * 60 + 30, currentMinA: 6, currentMaxA: 22, currentMaxAutoA: 22, gridCurrentMaxA: 20, acLoadsMaxSafeW: 8500, houseAcHeadroomW: 3500, endOfDaySocGuard: 30, endOfDaySocTarget: 45, pvStartMinW: 1000, pvHoldMinW: 700, probeEnabled: true, socStartMin: 80, socStopMin: 25, morningSocMin: 35, middaySocMin: 75, eveningSocStartMin: 85, eveningSocStopMin: 60, batteryDischargeSoftA: 0, batteryDischargeHardA: -15, morningBatteryMaxDischargeA: -5, middayBatteryMaxDischargeA: -8, eveningBatteryMaxDischargeA: -10, gridBatterySoftA: -3, gridBatteryHardA: -6, cloudDebtMaxKwh: 1.2, evcsNightBatteryHardFloorPct: 22, evcsNightStrongForecastKwh: 18, evcsNightMediumForecastKwh: 10, evcsNightTargetStrongForecastPct: 35, evcsNightTargetMediumForecastPct: 55, evcsNightTargetWeakForecastPct: 85, evcsNightBatteryRecoveryPowerKw: 1.5, evcsNightDepartureSafetyMin: 45, morningProbePvAvgMinW: 200, morningProbePvTrendMinW: 20, middayProbePvAvgMinW: 300, afternoonProbePvAvgMinW: 300, probeCurrentA: 10, }, history: { nightSessionStartSocPct: soc, nightSessionStartAt: '2026-08-07 01:24:00', nightSessionStartMs: Date.now() - 6 * 60000, lastBlockReason: 'none', blockedUntilAt: '', }, batteryTune: { global: { stepUpCooldownSeconds: 90, stepDownCooldownSeconds: 45 }, profile: {}, bands: { gt90: { dischargeSoftA: -50, dischargeHardA: -100, chargeReserveA: 5, softToleranceMin: 12, hardToleranceSec: 20, recoveryMin: 6 }, '80_90': { dischargeSoftA: -40, dischargeHardA: -80, chargeReserveA: 10, softToleranceMin: 10, hardToleranceSec: 20, recoveryMin: 6 }, '60_80': { dischargeSoftA: -20, dischargeHardA: -50, chargeReserveA: 40, softToleranceMin: 6, hardToleranceSec: 15, recoveryMin: 8 }, '40_60': { dischargeSoftA: -12, dischargeHardA: -30, chargeReserveA: 70, softToleranceMin: 4, hardToleranceSec: 10, recoveryMin: 10 }, '25_40': { dischargeSoftA: -5, dischargeHardA: -15, chargeReserveA: 110, softToleranceMin: 2, hardToleranceSec: 5, recoveryMin: 12 }, lt25: { dischargeSoftA: -3, dischargeHardA: -8, chargeReserveA: 150, softToleranceMin: 1, hardToleranceSec: 2, recoveryMin: 15 }, }, }, }; } function run(code, prev) { const ctx = { $input: { first: () => ({ json: prev }) }, console, Date, Intl, Math, Number, Object, String, JSON, }; vm.createContext(ctx); const result = vm.runInContext(`(function(){\n${code}\n})()`, ctx); return result[0].json; } const scenarios = [ { label: 'Nuit HC 01:30, SOC 53, VW branchée, toggle HC nuit ON (nuit dernière)', soc: 53, allowGridHcNight: true, vehicleConnected: true, minNow: 90, hour: 1, batteryCurrentA: -14, takeoverPrev: false }, { label: 'Nuit HC 01:30, SOC 53, VW branchée, toggle HC nuit OFF (comportement historique)', soc: 53, allowGridHcNight: false, vehicleConnected: true, minNow: 90, hour: 1, batteryCurrentA: -14, takeoverPrev: false }, { label: 'Nuit HC 01:30, SOC 30 (< cible 35), branchée, toggle ON', soc: 30, allowGridHcNight: true, vehicleConnected: true, minNow: 90, hour: 1, batteryCurrentA: -14, takeoverPrev: false }, { label: 'Nuit HC 01:30, pas de véhicule, toggle ON', soc: 53, allowGridHcNight: true, vehicleConnected: false, minNow: 90, hour: 1, batteryCurrentA: -14, takeoverPrev: false }, { label: 'Jour 15:00, SOC 53, branchée, toggle ON (hors fenêtre nuit)', soc: 53, allowGridHcNight: true, vehicleConnected: true, minNow: 900, hour: 15, batteryCurrentA: 10, takeoverPrev: false }, { label: 'Nuit HC 03:00, takeover déjà engagé (latch), SOC 40, toggle ON', soc: 40, allowGridHcNight: true, vehicleConnected: true, minNow: 180, hour: 3, batteryCurrentA: 2, takeoverPrev: true }, { label: 'Nuit HC 02:00, EDF ON (cycle suivant), SOC 53, batterie tenue +2A, toggle ON', soc: 53, allowGridHcNight: true, vehicleConnected: true, minNow: 120, hour: 2, batteryCurrentA: 2, takeoverPrev: true, gridAvailable: true }, ]; for (const sc of scenarios) { const out = run(windowsCode, buildPrev(sc)); const n = out.nightEvcsPreview; console.log(`\n=== ${sc.label}`); console.log(` profile=${out.context.profile} | sourceMode=${out.context.sourceMode}`); console.log(` nightSessionActive=${n.nightSessionActive} | batteryAssistAllowed=${n.batteryAssistAllowed}`); console.log(` nightGridTakeoverNeeded=${n.nightGridTakeoverNeeded} | nightPreferredSourceMode=${n.nightPreferredSourceMode}`); console.log(` target=${n.batteryMorningTargetPct}% trigger=${n.gridTakeoverTriggerPct}%`); // Chaînage BATTERY_CLOUD_LOCKS sur la sortie de WINDOWS_SOURCEV2 const bCode = wf.nodes.find(x => x.name === 'BATTERY_CLOUD_LOCKS').parameters.jsCode; try { const bOut = run(bCode, out); const c = bOut.conditions || {}; const ne = bOut.nightEvcs || {}; console.log(` [BCL] nightPreferred=${ne.nightPreferredSourceMode} takeoverNeeded=${ne.nightGridTakeoverNeeded} | gridStartOk=${c.gridStartOk} (gridAvailable=${sc.gridAvailable ?? false}) | assistNightStartOk=${c.batteryAssistNightStartOk}`); } catch (e) { console.log(` [BCL] échec exécution: ${e.message}`); } }