// ===================================================== // 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), }); } else if (cloud.sessionObservedActive) { if (cloud.enteringCloudDebt) { setCloudDebtStartedAt = true; } if (conditions.shouldStopOverloadAtMin) { setOff({ stateName: 'vebus_overload_stop_min_current', decisionName: 'stop', reasonText: 'Arrêt: surcharge convertisseur persistante au courant mini', dynamic: false, mustStopFlag: true, blockReason: 'vebus_overload_persistent', blockedUntilStamp: formatParisStampFromMs( nowMs + batteryTune.global.overloadStopAfterSeconds * 1000 ), }); } else if (safeguards.gridBatteryProtectionStop) { setOff({ stateName: 'stop_grid_battery_guard', decisionName: 'stop', reasonText: `Arrêt charge réseau: batterie trop sollicitée (${round1(parsed.batteryCurrentA)}A)`, dynamic: false, mustStopFlag: true, blockReason: 'grid_battery_guard', blockedUntilStamp: formatParisStampFromMs(nowMs + 20 * 60000), }); } else if (conditions.probeFailedNow) { setOff({ stateName: 'probe_failed_lock', decisionName: 'stop', reasonText: 'Mode sonde échoué', dynamic: false, mustStopFlag: true, blockReason: 'probe_failed', blockedUntilStamp: formatParisStampFromMs(nowMs + params.probeRetryMinutes * 60000), }); } else if (derived.physicalCapA < params.currentMinA) { if (lifecycle.minRunElapsed) { setOff({ stateName: 'stop_power_cap', decisionName: 'stop_power_cap', reasonText: 'Capacité système insuffisante', dynamic: false, mustStopFlag: true, blockReason: 'system_capacity_stop', blockedUntilStamp: formatParisStampFromMs(nowMs + 10 * 60000), }); } else { setOn({ stateName: 'hold_min_run_power_cap', decisionName: 'hold', currentA: params.currentMinA, reasonText: "Maintien jusqu'au temps mini ON", dynamic: false, mustStopFlag: false, }); } } else if (conditions.probeWindowActive && !conditions.probeSuccessNow) { if (!conditions.probeHoldGuardOk) { setOff({ stateName: 'probe_stop_guard', decisionName: 'stop', reasonText: 'Sonde interrompue: garde-fous non respectés', dynamic: false, mustStopFlag: true, blockReason: 'probe_failed', blockedUntilStamp: formatParisStampFromMs(nowMs + params.probeRetryMinutes * 60000), }); } else { setOn({ stateName: 'probe_hold', decisionName: 'hold', currentA: Math.min(params.probeCurrentA, derived.physicalCapA), reasonText: `Mode sonde actif à ${round1(Math.min(params.probeCurrentA, derived.physicalCapA))}A`, dynamic: true, mustStopFlag: false, }); } } else { const activeSourceMode = conditions.probeSuccessNow ? 'solar_only' : context.sourceMode; const holdConditionsOk = (activeSourceMode === 'grid_hc_allowed' || activeSourceMode === 'grid_forced') ? conditions.gridHoldOk : (activeSourceMode === 'solar_only' || activeSourceMode === 'solar_potential') ? conditions.solarHoldOk : (activeSourceMode === 'hybrid_probe') ? conditions.probeHoldGuardOk : false; if (activeSourceMode === 'blocked') { if (lifecycle.minRunElapsed) { setOff({ stateName: 'stop_source_lost', decisionName: 'stop', reasonText: 'Source autorisée perdue', dynamic: false, mustStopFlag: true, blockReason: context.profile === 'outside_window' ? 'outside_window' : 'source_unavailable', blockedUntilStamp: context.profile === 'outside_window' && Number.isFinite(locks.nextAllowedWindowStartMs) ? formatParisStampFromMs(locks.nextAllowedWindowStartMs) : formatParisStampFromMs(nowMs + params.minStopMinutes * 60000), }); } else { setOn({ stateName: 'hold_min_run_source_lost', decisionName: 'hold', currentA: Math.min(derived.currentReferenceA, derived.physicalCapA), reasonText: "Source perdue mais maintien jusqu'au mini ON", dynamic: false, mustStopFlag: false, }); } } else if (!holdConditionsOk) { if (lifecycle.minRunElapsed) { setOff({ stateName: 'stop_conditions_lost', decisionName: 'stop', reasonText: 'Conditions de maintien perdues', dynamic: false, mustStopFlag: true, blockReason: 'hold_conditions_lost', blockedUntilStamp: formatParisStampFromMs(nowMs + 10 * 60000), }); } else { setOn({ stateName: 'hold_min_run', decisionName: 'hold', currentA: Math.min(derived.currentReferenceA, derived.physicalCapA), reasonText: "Maintien jusqu'au temps mini ON", dynamic: true, mustStopFlag: false, }); } } else if ((derived.currentReferenceA > derived.physicalCapA) && lifecycle.currentCooldownElapsed) { setOn({ stateName: 'charging_step_down_system_cap', decisionName: 'step_down', currentA: Math.max( params.currentMinA, Math.min( derived.currentReferenceA - Math.max(params.currentStepDownA, 2), derived.physicalCapA ) ), reasonText: 'Réduction pour respecter la capacité système', dynamic: true, mustStopFlag: false, }); } else if (conditions.shouldStepDownOverload) { setOn({ stateName: 'charging_step_down_overload', decisionName: 'step_down_fast', currentA: Math.max( params.currentMinA, Math.min( derived.currentReferenceA - Math.max(batteryTune.global.overloadStepDownA, params.currentStepDownA), derived.physicalCapA ) ), reasonText: 'Baisse rapide: surcharge convertisseur', dynamic: true, mustStopFlag: false, }); } else if (conditions.shouldStepDownVebusPower) { setOn({ stateName: 'charging_step_down_vebus_power', decisionName: 'step_down_fast', currentA: Math.max( params.currentMinA, Math.min( derived.currentReferenceA - Math.max(params.currentStepDownA + 1, 3), derived.physicalCapA ) ), reasonText: 'Baisse rapide: puissance VE.Bus élevée', dynamic: true, mustStopFlag: false, }); } 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 === 'grid_hc_allowed' || activeSourceMode === 'grid_forced') && conditions.gridShouldStepDownBattery ) { setOn({ stateName: 'charging_step_down_grid_guard', decisionName: 'step_down', currentA: Math.max( params.currentMinA, Math.min( derived.currentReferenceA - params.currentStepDownA, derived.physicalCapA ) ), reasonText: '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' ? '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' ? '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 === '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 === '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: 'Maintien du courant', dynamic: true, mustStopFlag: false, }); } } }