// === DECISION INIT === let desiredCharging = false; let desiredPermission = false; let desiredLatch = false; let desiredCurrentA = currentMinA; let state = 'idle'; let currentDecision = 'hold'; let dynamicOk = false; let mustStop = false; let reason = []; let setLastStartAt = false; let setLastStopAt = false; let setLastCurrentChangeAt = false; let setCloudDebtStartedAt = false; let setProbeStartedAt = false; let notifyWanted = false; let notifyTitle = 'Victron EVCS Surplus'; let notifyMessage = ''; let nextBlockReason = 'none'; let nextBlockedUntilStamp = now.stamp; // === DECISION TREE (WITH STRATEGY LAYERS) === if (!controllerEnabled) { desiredCharging = evcsChargingSwitchOn; desiredPermission = chargePermissionNow; desiredLatch = controlLatch; desiredCurrentA = clamp(currentReferenceA || currentMinA, currentMinA, currentMaxAutoA); state = 'controller_disabled'; reason.push("Contrôleur désactivé"); } else if (isStopMode) { desiredCharging = false; desiredPermission = false; desiredLatch = false; desiredCurrentA = currentMinA; state = 'stopped_user'; currentDecision = 'stop_stop_mode'; dynamicOk = false; mustStop = false; nextBlockReason = 'manual_stop'; nextBlockedUntilStamp = now.stamp; reason.push("Arrêt manuel via Stratégie STOP"); } else if (evcsModeIsAuto) { desiredCharging = false; desiredPermission = false; desiredLatch = false; desiredCurrentA = currentMinA; state = 'mode_auto_conflict'; currentDecision = 'stop_auto_mode_conflict'; dynamicOk = false; mustStop = true; reason.push("Borne en mode Auto interne"); } else if (!evcsDeviceConnected || !evcsVehicleConnected) { desiredCharging = false; desiredPermission = false; desiredLatch = false; desiredCurrentA = currentMinA; state = 'vehicle_not_connected'; currentDecision = 'stop_not_connected'; dynamicOk = false; mustStop = false; nextBlockReason = 'vehicle_not_connected'; nextBlockedUntilStamp = now.stamp; reason.push("Véhicule ou borne non connecté"); } else if (mustStopHard) { desiredCharging = false; desiredPermission = false; desiredLatch = false; desiredCurrentA = currentMinA; state = anyVebusOverload ? 'vebus_overload_hard_stop' : anyVebusLowBattery ? 'vebus_low_battery_hard_stop' : vebusDcVoltageHardLow ? 'vebus_dc_voltage_hard_stop' : vebusAcOutHardReached ? 'vebus_ac_hard_stop' : batteryCurrentHardExceeded ? 'battery_current_hard_stop' : 'hard_stop'; currentDecision = 'stop_hard'; dynamicOk = false; mustStop = true; nextBlockReason = 'battery_protection_stop'; nextBlockedUntilStamp = formatParisStampFromMs(nowMs + 15 * 60000); reason.push("Hard stop de protection"); } else if (sessionObservedActive) { if (enteringCloudDebt) { setCloudDebtStartedAt = true; } if (gridBatteryProtectionStop) { desiredCharging = false; desiredPermission = false; desiredLatch = false; desiredCurrentA = currentMinA; state = 'stop_grid_battery_guard'; currentDecision = 'stop'; dynamicOk = false; mustStop = true; nextBlockReason = 'grid_battery_guard'; nextBlockedUntilStamp = formatParisStampFromMs(nowMs + 20 * 60000); reason.push(`Arrêt HC réseau: batterie trop sollicitée (${round1(batteryCurrentA)}A)`); } else if (probeFailedNow) { desiredCharging = false; desiredPermission = false; desiredLatch = false; desiredCurrentA = currentMinA; state = 'probe_failed_lock'; currentDecision = 'stop'; dynamicOk = false; mustStop = true; nextBlockReason = 'probe_failed'; nextBlockedUntilStamp = formatParisStampFromMs(nowMs + probeRetryMinutes * 60000); reason.push("Mode sonde échoué"); } else if (physicalCapA < currentMinA) { if (minRunElapsed) { desiredCharging = false; desiredPermission = false; desiredLatch = false; desiredCurrentA = currentMinA; state = 'stop_power_cap'; currentDecision = 'stop_power_cap'; dynamicOk = false; mustStop = true; nextBlockReason = 'battery_protection_stop'; nextBlockedUntilStamp = formatParisStampFromMs(nowMs + 10 * 60000); reason.push("Capacité système insuffisante"); } else { desiredCharging = true; desiredPermission = true; desiredLatch = true; desiredCurrentA = currentMinA; state = 'hold_min_run_power_cap'; currentDecision = 'hold'; dynamicOk = false; mustStop = false; reason.push("Maintien jusqu'au temps mini ON"); } } else if (probeWindowActive && !probeSuccessNow) { if (!probeHoldGuardOk) { desiredCharging = false; desiredPermission = false; desiredLatch = false; desiredCurrentA = currentMinA; state = 'probe_stop_guard'; currentDecision = 'stop'; dynamicOk = false; mustStop = true; nextBlockReason = 'probe_failed'; nextBlockedUntilStamp = formatParisStampFromMs(nowMs + probeRetryMinutes * 60000); reason.push("Sonde interrompue: garde-fous non respectés"); } else { desiredCharging = true; desiredPermission = true; desiredLatch = true; desiredCurrentA = clamp(probeCurrentA, currentMinA, physicalCapA); state = 'probe_hold'; currentDecision = 'hold'; dynamicOk = true; mustStop = false; reason.push(`Mode sonde actif à ${round1(desiredCurrentA)}A`); } } else { const activeSourceMode = probeSuccessNow ? 'solar_only' : sourceMode; const holdConditionsOk = activeSourceMode === 'grid_hc_allowed' || activeSourceMode === 'grid_forced' ? gridHoldOk : // Ajout ici aussi activeSourceMode === 'solar_only' || activeSourceMode === 'solar_potential' ? solarHoldOk : probeHoldGuardOk; if (activeSourceMode === 'blocked') { if (minRunElapsed) { desiredCharging = false; desiredPermission = false; desiredLatch = false; desiredCurrentA = currentMinA; state = 'stop_source_lost'; currentDecision = 'stop'; dynamicOk = false; mustStop = true; nextBlockReason = profile === 'outside_window' ? 'outside_window' : 'source_unavailable'; nextBlockedUntilStamp = profile === 'outside_window' && nextAllowedWindowStartMs !== null ? formatParisStampFromMs(nextAllowedWindowStartMs) : formatParisStampFromMs(nowMs + minStopMinutes * 60000); reason.push("Source autorisée perdue"); } else { desiredCharging = true; desiredPermission = true; desiredLatch = true; desiredCurrentA = clamp(Math.min(currentReferenceA, physicalCapA), currentMinA, physicalCapA); state = 'hold_min_run_source_lost'; currentDecision = 'hold'; dynamicOk = false; mustStop = false; reason.push("Source perdue mais maintien jusqu'au mini ON"); } } else if (!holdConditionsOk) { if (minRunElapsed) { desiredCharging = false; desiredPermission = false; desiredLatch = false; desiredCurrentA = currentMinA; state = 'stop_conditions_lost'; currentDecision = 'stop'; dynamicOk = false; mustStop = true; nextBlockReason = 'battery_protection_stop'; nextBlockedUntilStamp = formatParisStampFromMs(nowMs + 10 * 60000); reason.push("Conditions de maintien perdues"); } else { desiredCharging = true; desiredPermission = true; desiredLatch = true; desiredCurrentA = clamp(Math.min(currentReferenceA, physicalCapA), currentMinA, physicalCapA); state = 'hold_min_run'; currentDecision = 'hold'; dynamicOk = true; mustStop = false; reason.push("Maintien jusqu'au temps mini ON"); } } else if ((currentReferenceA > physicalCapA) && currentCooldownElapsed) { desiredCharging = true; desiredPermission = true; desiredLatch = true; desiredCurrentA = Math.max( currentMinA, Math.min(currentReferenceA - Math.max(currentStepDownA, 2), physicalCapA) ); state = 'charging_step_down_system_cap'; currentDecision = 'step_down'; dynamicOk = true; reason.push("Réduction pour respecter la capacité système"); } else if (shouldStepDownVebusPower) { desiredCharging = true; desiredPermission = true; desiredLatch = true; desiredCurrentA = Math.max( currentMinA, Math.min(currentReferenceA - Math.max(currentStepDownA + 1, 3), physicalCapA) ); state = 'charging_step_down_vebus_power'; currentDecision = 'step_down_fast'; dynamicOk = true; reason.push("Baisse rapide: puissance VE.Bus élevée"); } else if (shouldStepDownVebusVoltage) { desiredCharging = true; desiredPermission = true; desiredLatch = true; desiredCurrentA = Math.max( currentMinA, Math.min(currentReferenceA - Math.max(currentStepDownA + 1, 3), physicalCapA) ); state = 'charging_step_down_dc_voltage'; currentDecision = 'step_down_fast'; dynamicOk = true; reason.push("Baisse rapide: tension DC basse"); } else if (activeSourceMode === 'grid_hc_allowed' && gridShouldStepDownBattery) { desiredCharging = true; desiredPermission = true; desiredLatch = true; desiredCurrentA = Math.max( currentMinA, Math.min(currentReferenceA - currentStepDownA, physicalCapA) ); state = 'charging_step_down_grid_guard'; currentDecision = 'step_down'; dynamicOk = true; reason.push("Réduction HC: la batterie commence à participer"); } else if (activeSourceMode === 'solar_only' && solarShouldStepDownCloudDebt) { desiredCharging = true; desiredPermission = true; desiredLatch = true; desiredCurrentA = Math.max( currentMinA, Math.min(currentReferenceA - Math.max(currentStepDownA, 2), physicalCapA) ); state = 'charging_step_down_cloud_debt'; currentDecision = 'step_down'; dynamicOk = true; reason.push("Réduction: dette nuage trop élevée"); } else if (activeSourceMode === 'solar_only' && solarShouldStepDownBattery) { desiredCharging = true; desiredPermission = true; desiredLatch = true; desiredCurrentA = Math.max( currentMinA, Math.min(currentReferenceA - currentStepDownA, physicalCapA) ); state = 'charging_step_down_battery'; currentDecision = 'step_down'; dynamicOk = true; reason.push("Réduction: batterie trop sollicitée"); } else if (activeSourceMode === 'grid_hc_allowed' && gridShouldStepUp) { desiredCharging = true; desiredPermission = true; desiredLatch = true; desiredCurrentA = clamp(currentReferenceA + currentStepUpA, currentMinA, physicalCapA); state = 'charging_step_up_grid_hc'; currentDecision = 'step_up'; dynamicOk = true; reason.push("Hausse prudente du courant en HC réseau"); } else if (activeSourceMode === 'solar_only' || activeSourceMode === 'solar_potential') { // Potentiel autorise step up aussi desiredCharging = true; desiredPermission = true; desiredLatch = true; desiredCurrentA = clamp(currentReferenceA + currentStepUpA, currentMinA, physicalCapA); state = probeSuccessNow ? 'probe_success_step_up' : (batteryPriorityMode ? 'charging_step_up_battery' : 'charging_step_up_forecast'); currentDecision = 'step_up'; dynamicOk = true; reason.push("Hausse prudente du courant"); } else { desiredCharging = true; desiredPermission = true; desiredLatch = true; desiredCurrentA = clamp(Math.min(currentReferenceA, physicalCapA), currentMinA, physicalCapA); state = probeSuccessNow ? 'probe_success_hold' : activeSourceMode === 'grid_hc_allowed' ? 'charging_hold_grid_hc' : cloudDebtActive ? 'charging_hold_cloud_tolerance' : 'charging_hold'; currentDecision = 'hold'; dynamicOk = true; mustStop = false; reason.push("Maintien du courant"); } } } else { const startConditionsOk = (sourceMode === 'grid_hc_allowed' || sourceMode === 'grid_forced') ? gridStartOk : // Ajout de 'grid_forced' sourceMode === 'solar_only' || sourceMode === 'solar_potential' ? solarStartOk : // Potentiel autorise démarrage aussi sourceMode === 'hybrid_probe' ? probeStartOk : false; if (profile === 'outside_window') { // outside_window n'est plus un mur absolu en mode force ou avec potentiel if (sourceMode === 'solar_potential' || isForceMode) { // Continue à chercher startConditionsOk... } else { desiredCharging = false; desiredPermission = false; desiredLatch = false; desiredCurrentA = currentMinA; state = 'blocked_outside_window'; currentDecision = 'hold'; dynamicOk = false; mustStop = false; nextBlockReason = 'outside_window'; nextBlockedUntilStamp = nextAllowedWindowStartMs !== null ? formatParisStampFromMs(nextAllowedWindowStartMs) : now.stamp; reason.push("Hors fenêtre autorisée (pas de potentiel confirmé ni forcé)"); } } else if (!canRunAtMinCurrent) { desiredCharging = false; desiredPermission = false; desiredLatch = false; desiredCurrentA = currentMinA; state = 'blocked_by_capacity_min_current'; currentDecision = 'hold'; dynamicOk = false; mustStop = false; nextBlockReason = 'battery_protection_stop'; nextBlockedUntilStamp = formatParisStampFromMs(nowMs + 10 * 60000); reason.push("Capacité insuffisante pour courant mini"); } else if (sourceMode === 'blocked') { desiredCharging = false; desiredPermission = false; desiredLatch = false; desiredCurrentA = currentMinA; state = profile === 'night_hc' ? 'night_no_grid_hc_lock' : 'idle_wait_source'; currentDecision = 'hold'; dynamicOk = false; mustStop = false; nextBlockReason = profile === 'night_hc' ? 'outside_window' : 'source_unavailable'; nextBlockedUntilStamp = profile === 'night_hc' && nextAllowedWindowStartMs !== null ? formatParisStampFromMs(nextAllowedWindowStartMs) : formatParisStampFromMs(nowMs + 15 * 60000); reason.push(profile === 'night_hc' ? "Nuit sans autorisation HC réseau" : "Source non autorisée ou insuffisante"); } else if (startBlockedByReason) { desiredCharging = false; desiredPermission = false; desiredLatch = false; desiredCurrentA = currentMinA; state = 'wait_reason_lock'; currentDecision = 'hold'; dynamicOk = false; mustStop = false; nextBlockReason = lastBlockReason; nextBlockedUntilStamp = blockedUntilAt || now.stamp; reason.push(`Blocage anti-cliquetis: ${lastBlockReason}`); } else if (!startConditionsOk) { desiredCharging = false; desiredPermission = false; desiredLatch = false; desiredCurrentA = currentMinA; state = 'idle_wait_conditions'; currentDecision = 'hold'; dynamicOk = false; mustStop = false; reason.push("Conditions de démarrage non réunies"); } else if (!minStopElapsed) { desiredCharging = false; desiredPermission = false; desiredLatch = false; desiredCurrentA = currentMinA; state = 'wait_min_stop'; currentDecision = 'hold'; dynamicOk = false; mustStop = false; reason.push("Temps mini OFF non écoulé"); } else { desiredCharging = true; desiredPermission = true; desiredLatch = true; desiredCurrentA = sourceMode === 'hybrid_probe' ? clamp(probeCurrentA, currentMinA, physicalCapA) : currentMinA; state = sourceMode === 'grid_hc_allowed' ? 'start_grid_hc_min_current' : sourceMode === 'hybrid_probe' ? 'probe_start_6a' : sourceMode === 'solar_potential' ? 'start_forecast_solar_potential' : // NOUVEAU STATE batteryFullHarvestMode ? 'start_min_current_battery_full_harvest' : forecastCommitMode ? 'start_min_current_forecast_commit' : 'start_min_current'; currentDecision = 'start'; dynamicOk = true; mustStop = false; nextBlockReason = 'none'; // Clean blocking logic: set reason to none on start nextBlockedUntilStamp = now.stamp; setProbeStartedAt = sourceMode === 'hybrid_probe'; reason.push( sourceMode === 'grid_hc_allowed' ? "Démarrage en fenêtre HC réseau" : sourceMode === 'hybrid_probe' ? "Démarrage en mode sonde" : sourceMode === 'solar_potential' ? "Démarrage par potentiel solaire confirmé (SOC>=85%)" // NOUVEAU REASON TEXT : batteryFullHarvestMode ? "Démarrage harvest solaire" : forecastCommitMode ? "Démarrage forecast commit" : "Démarrage prudent à courant mini" ); } }