- 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.
130 lines
2.7 KiB
JavaScript
130 lines
2.7 KiB
JavaScript
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,
|
|
} = 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;
|
|
|
|
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);
|
|
}
|