version 2.2
This commit is contained in:
468
decoupesurplus/DECISION_ENGINE.js
Normal file
468
decoupesurplus/DECISION_ENGINE.js
Normal file
@@ -0,0 +1,468 @@
|
||||
// === 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user