function round1(v) { return Math.round(v * 10) / 10; } function round2(v) { return Math.round(v * 100) / 100; } function clamp(v, min, max) { return Math.min(Math.max(v, min), max); } 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 { input, now, nowMs, cfg, parsed, params, history, strategy, windows, context, derived, batteryTune, batteryPolicy, locks, cloud, modes, safeguards, conditions, decision, } = prev; // ===================================================== // FINAL ACTIONS // ===================================================== let desiredCurrentA = decision.desiredCurrentA; desiredCurrentA = decision.desiredCharging ? clamp( desiredCurrentA, params.currentMinA, Math.max(params.currentMinA, derived.physicalCapA || params.currentMinA) ) : params.currentMinA; const chargingAction = decision.desiredCharging === parsed.evcsChargingSwitchOn ? 'none' : decision.desiredCharging ? 'turn_on' : 'turn_off'; const permissionAction = decision.desiredPermission === parsed.chargePermissionNow ? 'none' : decision.desiredPermission ? 'turn_on' : 'turn_off'; const latchAction = decision.desiredLatch === parsed.controlLatch ? 'none' : decision.desiredLatch ? 'turn_on' : 'turn_off'; const autoStartAction = parsed.controllerEnabled && parsed.evcsVehicleConnected && parsed.evcsAutoStartSwitchOn ? 'turn_off' : 'none'; const currentAction = decision.desiredCharging && ( !Number.isFinite(derived.currentReferenceA) || Math.abs(desiredCurrentA - derived.currentReferenceA) >= 0.5 ) ? 'set' : 'none'; const setLastStartAt = chargingAction === 'turn_on'; const setLastStopAt = chargingAction === 'turn_off'; const setLastCurrentChangeAt = currentAction === 'set'; // ===================================================== // CLEAN BLOCKING LOGIC // ===================================================== const blockedUntilToWrite = decision.nextBlockReason === 'none' ? now.stamp : decision.nextBlockedUntilStamp; const needsBlockReasonUpdate = decision.nextBlockReason !== history.lastBlockReason; // ===================================================== // GLOBAL EVCS PRIORITY // AUTO + OVERRIDE MANUEL // ===================================================== const forceWindowMinutes = Math.max( 30, batteryTune?.global?.forceModeAutoRevertMinutes ?? 120 ); const activeChargeIntent = decision.desiredCharging || ['start', 'hold', 'step_up', 'step_down', 'step_down_fast'].includes(decision.currentDecision); // ------------------------- // PRIORITÉ AUTO // ------------------------- let autoEvcsPriorityActive = false; let autoEnergyPriorityMode = 'normal'; let autoEnergyPriorityReason = 'Aucune priorité EVCS'; let autoEvcsPriorityUntil = now.stamp; if (parsed.evcsVehicleConnected && !strategy.isStopMode) { if (strategy.forceChargeRequested || strategy.forceGridRequested) { autoEvcsPriorityActive = true; autoEnergyPriorityMode = 'evcs_priority'; autoEnergyPriorityReason = strategy.forceGridRequested ? 'EVCS prioritaire : force_grid_now' : 'EVCS prioritaire : force_charge_now'; autoEvcsPriorityUntil = formatParisStampFromMs(nowMs + forceWindowMinutes * 60000); } else if (context.departurePriorityActive && Number.isFinite(prev.lifecycle.minutesUntilDeparture)) { autoEvcsPriorityActive = true; autoEnergyPriorityMode = 'evcs_priority'; autoEnergyPriorityReason = `EVCS prioritaire : départ dans ${Math.round(prev.lifecycle.minutesUntilDeparture)} min`; autoEvcsPriorityUntil = formatParisStampFromMs( nowMs + Math.max(1, prev.lifecycle.minutesUntilDeparture) * 60000 ); } else if (activeChargeIntent && context.profile === 'midday_hc_or_solar') { autoEvcsPriorityActive = true; autoEnergyPriorityMode = 'evcs_priority'; autoEnergyPriorityReason = 'EVCS prioritaire : recharge active fenêtre midi'; autoEvcsPriorityUntil = formatParisStampFromMs(nowMs + 90 * 60000); } } // ------------------------- // OVERRIDE MANUEL // ------------------------- const manualPriorityOverrideActive = parsed.evcsPriorityManualOverride && parsed.evcsPriorityManualMode === 'evcs_priority' && history.evcsPriorityManualUntilMs !== null && history.evcsPriorityManualUntilMs > nowMs + 1000; let evcsPriorityActive = autoEvcsPriorityActive; let energyPriorityMode = autoEnergyPriorityMode; let energyPriorityReason = autoEnergyPriorityReason; let evcsPriorityUntil = autoEvcsPriorityUntil; let evcsPrioritySource = 'auto'; if (manualPriorityOverrideActive) { evcsPriorityActive = true; energyPriorityMode = 'evcs_priority'; energyPriorityReason = parsed.evcsPriorityManualReason || 'EVCS prioritaire : override manuel'; evcsPriorityUntil = history.evcsPriorityManualUntilAt || autoEvcsPriorityUntil; evcsPrioritySource = 'manual'; }