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:
224
archive/decoupesurplus/V2/DECISION/decisionP1.js
Normal file
224
archive/decoupesurplus/V2/DECISION/decisionP1.js
Normal file
@@ -0,0 +1,224 @@
|
||||
function clamp(v, min, max) {
|
||||
return Math.min(Math.max(v, min), max);
|
||||
}
|
||||
|
||||
function round1(v) {
|
||||
return Math.round(v * 10) / 10;
|
||||
}
|
||||
|
||||
function to2(n) {
|
||||
return String(n).padStart(2, '0');
|
||||
}
|
||||
|
||||
function formatParisStampFromMs(ms) {
|
||||
const fmt = new Intl.DateTimeFormat('fr-FR', {
|
||||
timeZone: 'Europe/Paris',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
|
||||
const parts = Object.fromEntries(
|
||||
fmt.formatToParts(new Date(ms))
|
||||
.filter(p => p.type !== 'literal')
|
||||
.map(p => [p.type, p.value])
|
||||
);
|
||||
|
||||
const hour = Number(parts.hour) === 24 ? 0 : Number(parts.hour);
|
||||
return `${parts.year}-${parts.month}-${parts.day} ${to2(hour)}:${parts.minute}:${parts.second}`;
|
||||
}
|
||||
|
||||
const prev = $input.first().json;
|
||||
|
||||
const {
|
||||
now,
|
||||
nowMs,
|
||||
parsed,
|
||||
params,
|
||||
history,
|
||||
strategy,
|
||||
lifecycle,
|
||||
context,
|
||||
derived,
|
||||
locks,
|
||||
cloud,
|
||||
modes,
|
||||
safeguards,
|
||||
conditions,
|
||||
batteryTune,
|
||||
nightEvcs,
|
||||
} = prev;
|
||||
|
||||
// =====================================================
|
||||
// DECISION INIT
|
||||
// =====================================================
|
||||
let desiredCharging = false;
|
||||
let desiredPermission = false;
|
||||
let desiredLatch = false;
|
||||
let desiredCurrentA = params.currentMinA;
|
||||
|
||||
let state = 'idle';
|
||||
let currentDecision = 'hold';
|
||||
let dynamicOk = false;
|
||||
let mustStop = false;
|
||||
|
||||
let reason = [];
|
||||
|
||||
let setCloudDebtStartedAt = false;
|
||||
let setProbeStartedAt = false;
|
||||
|
||||
// EVCS night session memory
|
||||
let setNightSessionStart = false;
|
||||
let clearNightSessionStart = false;
|
||||
|
||||
let notifyWanted = false;
|
||||
let notifyTitle = 'Victron EVCS Surplus';
|
||||
let notifyMessage = '';
|
||||
|
||||
let nextBlockReason = 'none';
|
||||
let nextBlockedUntilStamp = now.stamp;
|
||||
|
||||
// =====================================================
|
||||
// HELPERS
|
||||
// =====================================================
|
||||
function setOff({
|
||||
stateName,
|
||||
decisionName = 'stop',
|
||||
reasonText,
|
||||
dynamic = false,
|
||||
mustStopFlag = false,
|
||||
blockReason = 'none',
|
||||
blockedUntilStamp = now.stamp,
|
||||
}) {
|
||||
desiredCharging = false;
|
||||
desiredPermission = false;
|
||||
desiredLatch = false;
|
||||
desiredCurrentA = params.currentMinA;
|
||||
|
||||
state = stateName;
|
||||
currentDecision = decisionName;
|
||||
dynamicOk = dynamic;
|
||||
mustStop = mustStopFlag;
|
||||
nextBlockReason = blockReason;
|
||||
nextBlockedUntilStamp = blockedUntilStamp;
|
||||
|
||||
if (reasonText) reason.push(reasonText);
|
||||
}
|
||||
|
||||
function setOn({
|
||||
stateName,
|
||||
decisionName = 'hold',
|
||||
currentA = params.currentMinA,
|
||||
reasonText,
|
||||
dynamic = true,
|
||||
mustStopFlag = false,
|
||||
}) {
|
||||
desiredCharging = true;
|
||||
desiredPermission = true;
|
||||
desiredLatch = true;
|
||||
desiredCurrentA = clamp(
|
||||
currentA,
|
||||
params.currentMinA,
|
||||
Math.max(params.currentMinA, derived.physicalCapA || params.currentMinA)
|
||||
);
|
||||
|
||||
state = stateName;
|
||||
currentDecision = decisionName;
|
||||
dynamicOk = dynamic;
|
||||
mustStop = mustStopFlag;
|
||||
|
||||
if (reasonText) reason.push(reasonText);
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// SESSION MEMORY SIDE EFFECTS
|
||||
// =====================================================
|
||||
if (nightEvcs?.nightSessionNeedsCapture) {
|
||||
setNightSessionStart = true;
|
||||
}
|
||||
|
||||
if (nightEvcs?.nightSessionNeedsClear) {
|
||||
clearNightSessionStart = true;
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// 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),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user