correction logique commande priorisation

This commit is contained in:
2026-05-01 18:34:35 +02:00
parent 26b27f5faa
commit 1776d2776f
11 changed files with 5394 additions and 1 deletions

View File

@@ -0,0 +1,313 @@
// Source effectivement livrable.
// IMPORTANT:
// on évite de conclure "voiture pleine" si en réalité la source n'est
// pas disponible (ex: attente grid takeover).
const sourceDeliverableForNoDemand =
(context.sourceMode === 'grid_hc_allowed' || context.sourceMode === 'grid_forced')
? !!parsed.gridAvailable
: (context.sourceMode === 'solar_only' || context.sourceMode === 'solar_potential')
? !!conditions.solarHoldOk || !!conditions.solarStartOk
: context.sourceMode === 'hybrid_probe'
? !!conditions.probeHoldGuardOk || !!conditions.probeSuccessNow || !!conditions.probeStartOk
: context.sourceMode === 'battery_assist_night'
? !!conditions.batteryAssistNightHoldOk || !!conditions.batteryAssistNightStartOk
: false;
const evcsSessionArmedForNoDemand =
!!cloud.sessionObservedActive &&
!!parsed.evcsVehicleConnected &&
!!parsed.evcsChargingSwitchOn &&
!!parsed.chargePermissionNow &&
!!parsed.controlLatch &&
commandCurrentA > 0 &&
sourceDeliverableForNoDemand;
const noDemandScoreRaw = evcsSessionArmedForNoDemand
? clamp(
noDemandEvidenceRaw * sessionArmFactor * commandFactor,
0,
1
)
: 0;
const noDemandScoreEma = ema(prevNoDemandScoreEma, noDemandScoreRaw, alphaScore);
const adaptiveDecisionCenter = clamp(
0.72 - 0.20 * deliveryNoiseEma,
0.55,
0.82
);
const adaptiveDecisionBand = clamp(
0.16 + 0.80 * deliveryNoiseEma,
0.12,
0.34
);
const noDemandScoreOn = clamp(
adaptiveDecisionCenter + adaptiveDecisionBand / 2,
0.60,
0.95
);
const noDemandScoreOff = clamp(
adaptiveDecisionCenter - adaptiveDecisionBand / 2,
0.15,
noDemandScoreOn - 0.05
);
let evcsVehicleNoDemandSemantic = prevNoDemandSemanticActive;
if (!evcsSessionArmedForNoDemand) {
evcsVehicleNoDemandSemantic = false;
} else if (prevNoDemandSemanticActive) {
evcsVehicleNoDemandSemantic = noDemandScoreEma >= noDemandScoreOff;
} else {
evcsVehicleNoDemandSemantic = noDemandScoreEma >= noDemandScoreOn;
}
const evcsNoDemandSinceMs =
evcsVehicleNoDemandSemantic
? (
prevNoDemandSemanticActive && Number.isFinite(prevNoDemandSinceMs)
? prevNoDemandSinceMs
: nowMs
)
: null;
const evcsNoDemandSinceMinutes =
Number.isFinite(evcsNoDemandSinceMs) && Number.isFinite(nowMs)
? Math.max(0, (nowMs - evcsNoDemandSinceMs) / 60000)
: null;
const vehicleDemandFilter = {
dtSec: round3(noDemandDtSec),
commandCurrentA: round3(commandCurrentA),
realCurrentA: round3(realCurrentA),
expectedPowerW: round3(expectedPowerW),
realPowerW: round3(realPowerW),
currentDeliveryRatioRaw: round3(currentDeliveryRatioRaw),
powerDeliveryRatioRaw: round3(powerDeliveryRatioRaw),
deliveryRatioRaw: round3(deliveryRatioRaw),
deliveryRatioEma: round3(deliveryRatioEma),
deliveryNoiseEma: round3(deliveryNoiseEma),
adaptiveQuietBand: round3(adaptiveQuietBand),
noDemandEvidenceRaw: round3(noDemandEvidenceRaw),
sessionElapsedSec: round3(sessionElapsedSec),
sessionArmFactor: round3(sessionArmFactor),
commandFactor: round3(commandFactor),
sourceDeliverableForNoDemand,
evcsSessionArmedForNoDemand,
noDemandScoreRaw: round3(noDemandScoreRaw),
noDemandScoreEma: round3(noDemandScoreEma),
noDemandScoreOn: round3(noDemandScoreOn),
noDemandScoreOff: round3(noDemandScoreOff),
evcsVehicleNoDemandSemantic,
evcsNoDemandSinceMs: Number.isFinite(evcsNoDemandSinceMs) ? evcsNoDemandSinceMs : null,
evcsNoDemandSinceMinutes: Number.isFinite(evcsNoDemandSinceMinutes)
? round3(evcsNoDemandSinceMinutes)
: null,
nextHistory: {
evcsDeliveryRatioEma: round3(deliveryRatioEma),
evcsDeliveryNoiseEma: round3(deliveryNoiseEma),
evcsNoDemandScoreEma: round3(noDemandScoreEma),
evcsNoDemandSemanticActive: evcsVehicleNoDemandSemantic,
evcsNoDemandSinceMs: Number.isFinite(evcsNoDemandSinceMs) ? evcsNoDemandSinceMs : null,
evcsNoDemandFilterEvalMs: nowMs
}
};
// =====================================================
// 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),
});
}
// =====================================================
// ACTIVE SESSION BRANCH
// =====================================================
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
: activeSourceMode === 'battery_assist_night'
? conditions.batteryAssistNightHoldOk
: false;