else if (conditions.shouldStepDownVebusVoltage) { setOn({ stateName: 'charging_step_down_dc_voltage', decisionName: 'step_down_fast', currentA: Math.max( params.currentMinA, Math.min( derived.currentReferenceA - Math.max(params.currentStepDownA + 1, 3), derived.physicalCapA ) ), reasonText: 'Baisse rapide: tension DC basse', dynamic: true, mustStopFlag: false, }); } else if ( activeSourceMode === 'battery_assist_night' && conditions.batteryAssistNightShouldStepDown ) { setOn({ stateName: 'charging_step_down_battery_assist_night', decisionName: 'step_down', currentA: Math.max( params.currentMinA, Math.min( derived.currentReferenceA - params.currentStepDownA, derived.physicalCapA ) ), reasonText: nightEvcs?.batteryRecoveryUrgent ? 'Réduction: approche départ, préparation takeover réseau' : 'Réduction: assistance batterie nuit devient trop coûteuse', dynamic: true, mustStopFlag: false, }); } else if ( (activeSourceMode === 'grid_hc_allowed' || activeSourceMode === 'grid_forced') && conditions.gridShouldStepDownBattery ) { setOn({ stateName: activeSourceMode === 'grid_forced' ? 'charging_step_down_grid_forced_guard' : 'charging_step_down_grid_guard', decisionName: 'step_down', currentA: Math.max( params.currentMinA, Math.min( derived.currentReferenceA - params.currentStepDownA, derived.physicalCapA ) ), reasonText: activeSourceMode === 'grid_forced' ? 'Réduction réseau forcé: la batterie commence à participer' : 'Réduction réseau: la batterie commence à participer', dynamic: true, mustStopFlag: false, }); } else if ( (activeSourceMode === 'solar_only' || activeSourceMode === 'solar_potential') && conditions.solarShouldStepDownCloudDebt ) { setOn({ stateName: 'charging_step_down_cloud_debt', decisionName: 'step_down', currentA: Math.max( params.currentMinA, Math.min( derived.currentReferenceA - Math.max(params.currentStepDownA, 2), derived.physicalCapA ) ), reasonText: 'Réduction: dette nuage trop élevée', dynamic: true, mustStopFlag: false, }); } else if ( (activeSourceMode === 'solar_only' || activeSourceMode === 'solar_potential') && conditions.solarShouldStepDownBattery ) { setOn({ stateName: 'charging_step_down_battery', decisionName: 'step_down', currentA: Math.max( params.currentMinA, Math.min( derived.currentReferenceA - params.currentStepDownA, derived.physicalCapA ) ), reasonText: 'Réduction: batterie trop sollicitée', dynamic: true, mustStopFlag: false, }); } else if ( (activeSourceMode === 'grid_hc_allowed' || activeSourceMode === 'grid_forced') && conditions.gridShouldStepUp ) { setOn({ stateName: activeSourceMode === 'grid_forced' ? ( nightEvcs?.batteryRecoveryUrgent ? 'night_departure_deadline_grid_forced' : 'charging_step_up_grid_forced' ) : 'charging_step_up_grid_hc', decisionName: 'step_up', currentA: clamp( derived.currentReferenceA + params.currentStepUpA, params.currentMinA, derived.physicalCapA ), reasonText: activeSourceMode === 'grid_forced' ? ( nightEvcs?.batteryRecoveryUrgent ? 'Hausse prudente: priorité départ, réseau forcé' : 'Hausse prudente du courant en charge réseau forcée' ) : 'Hausse prudente du courant en HC réseau', dynamic: true, mustStopFlag: false, }); } else if ( activeSourceMode === 'battery_assist_night' && conditions.batteryAssistNightShouldStepUp ) { setOn({ stateName: 'charging_step_up_battery_assist_night', decisionName: 'step_up', currentA: clamp( derived.currentReferenceA + params.currentStepUpA, params.currentMinA, derived.physicalCapA ), reasonText: 'Hausse prudente du courant en assistance batterie nuit', dynamic: true, mustStopFlag: false, }); } else if ( (activeSourceMode === 'solar_only' || activeSourceMode === 'solar_potential') && conditions.solarShouldStepUp ) { setOn({ stateName: conditions.probeSuccessNow ? 'probe_success_step_up' : (modes.batteryPriorityMode ? 'charging_step_up_battery' : 'charging_step_up_forecast'), decisionName: 'step_up', currentA: clamp( derived.currentReferenceA + params.currentStepUpA, params.currentMinA, derived.physicalCapA ), reasonText: 'Hausse prudente du courant', dynamic: true, mustStopFlag: false, }); } else { setOn({ stateName: conditions.probeSuccessNow ? 'probe_success_hold' : activeSourceMode === 'battery_assist_night' ? 'charging_hold_battery_assist_night' : (activeSourceMode === 'grid_hc_allowed' || activeSourceMode === 'grid_forced') ? 'charging_hold_grid' : cloud.cloudDebtActive ? 'charging_hold_cloud_tolerance' : 'charging_hold', decisionName: 'hold', currentA: Math.min(derived.currentReferenceA, derived.physicalCapA), reasonText: activeSourceMode === 'battery_assist_night' ? 'Maintien du courant en assistance batterie nuit' : 'Maintien du courant', dynamic: true, mustStopFlag: false, }); } } } // ===================================================== // START BRANCH // ===================================================== else { const startConditionsOk = (context.sourceMode === 'grid_hc_allowed' || context.sourceMode === 'grid_forced') ? conditions.gridStartOk : (context.sourceMode === 'solar_only' || context.sourceMode === 'solar_potential') ? conditions.solarStartOk : context.sourceMode === 'hybrid_probe' ? conditions.probeStartOk : context.sourceMode === 'battery_assist_night' ? conditions.batteryAssistNightStartOk : false; if (!derived.canRunAtMinCurrent) { setOff({ stateName: 'blocked_by_capacity_min_current', decisionName: 'hold', reasonText: 'Capacité insuffisante pour courant mini', dynamic: false, mustStopFlag: false, blockReason: 'system_capacity_stop', blockedUntilStamp: formatParisStampFromMs(nowMs + 10 * 60000), }); } else if (context.sourceMode === 'blocked') { setOff({ stateName: context.profile === 'outside_window' ? 'blocked_outside_window' : (context.profile === 'night_hc' ? 'night_no_grid_hc_lock' : 'idle_wait_source'), decisionName: 'hold', reasonText: context.profile === 'outside_window' ? 'Hors fenêtre préférée sans source suffisante' : context.profile === 'night_hc' ? 'Nuit sans autorisation HC réseau' : 'Source non autorisée ou insuffisante', dynamic: false, mustStopFlag: false, blockReason: context.profile === 'outside_window' ? 'outside_window' : context.profile === 'night_hc' ? 'grid_hc_unavailable' : 'source_unavailable', blockedUntilStamp: Number.isFinite(locks.nextAllowedWindowStartMs) ? formatParisStampFromMs(locks.nextAllowedWindowStartMs) : formatParisStampFromMs(nowMs + 15 * 60000), }); } else if ( (context.sourceMode === 'grid_hc_allowed' || context.sourceMode === 'grid_forced') && !parsed.gridAvailable ) { setOff({ stateName: context.profile === 'night_hc' ? 'night_wait_grid_takeover' : 'wait_grid_source', decisionName: 'hold', reasonText: context.profile === 'night_hc' ? ( nightEvcs?.batteryRecoveryUrgent ? 'Réseau demandé: urgence recharge batterie avant départ' : 'Prise de relais réseau demandée pour session nuit EVCS' ) : 'Source réseau demandée mais pas encore disponible', dynamic: false, mustStopFlag: false, blockReason: context.profile === 'night_hc' ? 'grid_hc_unavailable' : 'grid_unavailable', blockedUntilStamp: formatParisStampFromMs(nowMs + 10 * 60000), }); } else if (locks.startBlockedByReason) { setOff({ stateName: 'wait_reason_lock', decisionName: 'hold', reasonText: `Blocage anti-cliquetis: ${history.lastBlockReason}`, dynamic: false, mustStopFlag: false, blockReason: history.lastBlockReason, blockedUntilStamp: history.blockedUntilAt || now.stamp, }); } else if (!startConditionsOk) { setOff({ stateName: 'idle_wait_conditions', decisionName: 'hold', reasonText: context.sourceMode === 'battery_assist_night' ? 'Conditions de démarrage nuit batterie non réunies' : 'Conditions de démarrage non réunies', dynamic: false, mustStopFlag: false, blockReason: 'none', blockedUntilStamp: now.stamp, }); } else if (!lifecycle.minStopElapsed) { setOff({ stateName: 'wait_min_stop', decisionName: 'hold', reasonText: 'Temps mini OFF non écoulé', dynamic: false, mustStopFlag: false, blockReason: 'none', blockedUntilStamp: now.stamp, }); } else { const startCurrentA = context.sourceMode === 'hybrid_probe' ? clamp(params.probeCurrentA, params.currentMinA, derived.physicalCapA) : params.currentMinA; setOn({ stateName: context.sourceMode === 'grid_forced' ? ( nightEvcs?.batteryRecoveryUrgent ? 'night_departure_deadline_grid_forced' : 'start_grid_forced_min_current' ) : context.sourceMode === 'grid_hc_allowed' ? 'start_grid_hc_min_current' : context.sourceMode === 'battery_assist_night' ? 'start_battery_assist_night_min_current' : context.sourceMode === 'hybrid_probe' ? 'probe_start_6a' : context.sourceMode === 'solar_potential' ? 'start_forecast_solar_potential' : modes.batteryFullHarvestMode ? 'start_min_current_battery_full_harvest' : modes.forecastCommitMode ? 'start_min_current_forecast_commit' : 'start_min_current', decisionName: 'start', currentA: startCurrentA, reasonText: context.sourceMode === 'grid_forced' ? ( nightEvcs?.batteryRecoveryUrgent ? 'Démarrage réseau forcé: priorité départ / recharge batterie' : 'Démarrage en charge réseau forcée' ) : context.sourceMode === 'grid_hc_allowed' ? 'Démarrage en fenêtre HC réseau' : context.sourceMode === 'battery_assist_night' ? 'Démarrage nuit en assistance batterie contrôlée' : context.sourceMode === 'hybrid_probe' ? 'Démarrage en mode sonde' : context.sourceMode === 'solar_potential' ? 'Démarrage par potentiel solaire confirmé' : modes.batteryFullHarvestMode ? 'Démarrage harvest solaire' : modes.forecastCommitMode ? 'Démarrage forecast commit' : 'Démarrage prudent à courant mini', dynamic: true, mustStopFlag: false, }); nextBlockReason = 'none'; nextBlockedUntilStamp = now.stamp; setProbeStartedAt = context.sourceMode === 'hybrid_probe'; } } // ===================================================== // CONTEXT REASONS // ===================================================== if (nightEvcs?.nightSessionActive) { reason.push( `Nuit EVCS: target=${round1(nightEvcs.batteryMorningTargetPct)}% | trigger réseau=${round1(nightEvcs.gridTakeoverTriggerPct)}%` ); if (Number.isFinite(nightEvcs.nightSessionStartSocPct)) { reason.push( `SOC départ session nuit=${round1(nightEvcs.nightSessionStartSocPct)}% | décharge autorisée=${round1(nightEvcs.maxBatteryDischargePoints)} pts` ); } if (nightEvcs.batteryRecoveryUrgent) { reason.push("Urgence départ: il faut anticiper la reprise réseau"); } } // ===================================================== // OUTPUT // ===================================================== return [{ json: { ...prev, decision: { desiredCharging, desiredPermission, desiredLatch, desiredCurrentA, state, currentDecision, dynamicOk, mustStop, reason, nextBlockReason, nextBlockedUntilStamp, setCloudDebtStartedAt, setProbeStartedAt, setNightSessionStart, clearNightSessionStart, notifyWanted, notifyTitle, notifyMessage, } } }];