function clamp(v, min, max) { return Math.min(Math.max(v, min), max); } function round1(v) { return Math.round(v * 10) / 10; } function to2(n) { return String(n).padStart(2, '0'); } function formatParisStampFromMs(ms) { const fmt = new Intl.DateTimeFormat('fr-FR', { timeZone: 'Europe/Paris', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }); const parts = Object.fromEntries( fmt.formatToParts(new Date(ms)) .filter(p => p.type !== 'literal') .map(p => [p.type, p.value]) ); const hour = Number(parts.hour) === 24 ? 0 : Number(parts.hour); return `${parts.year}-${parts.month}-${parts.day} ${to2(hour)}:${parts.minute}:${parts.second}`; } const prev = $input.first().json; const { now, nowMs, parsed, params, history, strategy, lifecycle, context, derived, locks, cloud, modes, safeguards, conditions, batteryTune, nightEvcs, } = prev; // ===================================================== // DECISION INIT // ===================================================== let desiredCharging = false; let desiredPermission = false; let desiredLatch = false; let desiredCurrentA = params.currentMinA; let state = 'idle'; let currentDecision = 'hold'; let dynamicOk = false; let mustStop = false; let reason = []; let setCloudDebtStartedAt = false; let setProbeStartedAt = false; // EVCS night session memory let setNightSessionStart = false; let clearNightSessionStart = false; let notifyWanted = false; let notifyTitle = 'Victron EVCS Surplus'; let notifyMessage = ''; let nextBlockReason = 'none'; let nextBlockedUntilStamp = now.stamp; // ===================================================== // HELPERS // ===================================================== function setOff({ stateName, decisionName = 'stop', reasonText, dynamic = false, mustStopFlag = false, blockReason = 'none', blockedUntilStamp = now.stamp, }) { desiredCharging = false; desiredPermission = false; desiredLatch = false; desiredCurrentA = params.currentMinA; state = stateName; currentDecision = decisionName; dynamicOk = dynamic; mustStop = mustStopFlag; nextBlockReason = blockReason; nextBlockedUntilStamp = blockedUntilStamp; if (reasonText) reason.push(reasonText); } function setOn({ stateName, decisionName = 'hold', currentA = params.currentMinA, reasonText, dynamic = true, mustStopFlag = false, }) { desiredCharging = true; desiredPermission = true; desiredLatch = true; desiredCurrentA = clamp( currentA, params.currentMinA, Math.max(params.currentMinA, derived.physicalCapA || params.currentMinA) ); state = stateName; currentDecision = decisionName; dynamicOk = dynamic; mustStop = mustStopFlag; if (reasonText) reason.push(reasonText); } // ===================================================== // SESSION MEMORY SIDE EFFECTS // ===================================================== if (nightEvcs?.nightSessionNeedsCapture) { setNightSessionStart = true; } if (nightEvcs?.nightSessionNeedsClear) { clearNightSessionStart = true; } // ===================================================== // DECISION TREE // ===================================================== if (!parsed.controllerEnabled) { desiredCharging = parsed.evcsChargingSwitchOn; desiredPermission = parsed.chargePermissionNow; desiredLatch = parsed.controlLatch; desiredCurrentA = clamp( derived.currentReferenceA || params.currentMinA, params.currentMinA, params.currentMaxAutoA ); state = 'controller_disabled'; currentDecision = 'hold'; dynamicOk = false; mustStop = false; reason.push('Contrôleur désactivé'); } else if (strategy.isStopMode) { setOff({ stateName: 'stopped_user', decisionName: 'stop_stop_mode', reasonText: 'Arrêt manuel via Stratégie STOP', dynamic: false, mustStopFlag: false, blockReason: 'manual_stop', blockedUntilStamp: now.stamp, }); } else if (parsed.evcsModeIsAuto) { setOff({ stateName: 'mode_auto_conflict', decisionName: 'stop_auto_mode_conflict', reasonText: 'Borne en mode Auto interne', dynamic: false, mustStopFlag: true, blockReason: 'mode_auto_conflict', blockedUntilStamp: now.stamp, }); } else if (!parsed.evcsDeviceConnected || !parsed.evcsVehicleConnected) { setOff({ stateName: 'vehicle_not_connected', decisionName: 'stop_not_connected', reasonText: 'Véhicule ou borne non connecté', dynamic: false, mustStopFlag: false, blockReason: 'vehicle_not_connected', blockedUntilStamp: now.stamp, }); } else if (safeguards.mustStopHard) { setOff({ stateName: derived.anyVebusLowBattery ? 'vebus_low_battery_hard_stop' : derived.vebusDcVoltageHardLow ? 'vebus_dc_voltage_hard_stop' : derived.vebusAcOutHardReached ? 'vebus_ac_hard_stop' : safeguards.batteryCurrentHardExceeded ? 'battery_current_hard_stop' : 'hard_stop', decisionName: 'stop_hard', reasonText: 'Hard stop de protection', dynamic: false, mustStopFlag: true, blockReason: safeguards.batteryCurrentHardExceeded ? 'battery_current_hard_stop' : derived.vebusDcVoltageHardLow ? 'vebus_dc_voltage_stop' : derived.anyVebusLowBattery ? 'vebus_low_battery_stop' : derived.vebusAcOutHardReached ? 'vebus_ac_hard_stop' : 'battery_protection_stop', blockedUntilStamp: formatParisStampFromMs(nowMs + 15 * 60000), }); }