R2: git = source de vérité — src/nodes/ + build/extract intégrés au push
- src/nodes/*.js (6 nodes + template snapshot) = source de vérité unique. - n8n-push.mjs reconstruit le JSON depuis src/ avant syntaxe+golden+PUT (src gagne toujours; n8n-extract.mjs pour resynchroniser si édition UI). - n8n-build.mjs --check pour vérifier la synchro. - Copies historiques déplacées dans archive/ (mortes, dérivées de la prod). - .gitignore: backups locaux de rollback. - AGENTS.md: règle R2 pour toutes les futures sessions (dont Qwen local). - Validé: idempotence, mutation/restauration, pipeline push complet en réel.
This commit is contained in:
313
archive/DECISION/decision_engineP2.js
Normal file
313
archive/DECISION/decision_engineP2.js
Normal 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;
|
||||
Reference in New Issue
Block a user