logique de traitement erron

This commit is contained in:
2026-05-01 17:38:40 +02:00
parent 874aca1afa
commit 5e6798b702
29 changed files with 9184 additions and 0 deletions

View File

@@ -0,0 +1,293 @@
function clamp(v, min, max) {
return Math.min(Math.max(v, min), max);
}
function to2(n) {
return String(n).padStart(2, '0');
}
function getParisParts(ms = Date.now()) {
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);
const date = `${parts.year}-${parts.month}-${parts.day}`;
return {
date,
hour,
minute: Number(parts.minute),
second: Number(parts.second),
};
}
function getTzOffsetMs(utcTs, timeZone = 'Europe/Paris') {
const dtf = new Intl.DateTimeFormat('en-US', {
timeZone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
});
const parts = Object.fromEntries(
dtf.formatToParts(new Date(utcTs))
.filter(p => p.type !== 'literal')
.map(p => [p.type, p.value])
);
const asIfUtc = Date.UTC(
Number(parts.year),
Number(parts.month) - 1,
Number(parts.day),
Number(parts.hour) === 24 ? 0 : Number(parts.hour),
Number(parts.minute),
Number(parts.second)
);
const d = new Date(utcTs);
const actualUtc = Date.UTC(
d.getUTCFullYear(),
d.getUTCMonth(),
d.getUTCDate(),
d.getUTCHours(),
d.getUTCMinutes(),
d.getUTCSeconds()
);
return asIfUtc - actualUtc;
}
function parseParisStampToMs(s) {
const text = (s ?? '').toString().trim();
const m = text.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
if (!m) return null;
const y = Number(m[1]);
const mo = Number(m[2]);
const d = Number(m[3]);
const h = Number(m[4]);
const mi = Number(m[5]);
const se = Number(m[6]);
const guessUtc = Date.UTC(y, mo - 1, d, h, mi, se);
const off1 = getTzOffsetMs(guessUtc, 'Europe/Paris');
let ts = guessUtc - off1;
const off2 = getTzOffsetMs(ts, 'Europe/Paris');
if (off2 !== off1) ts = guessUtc - off2;
return ts;
}
function hhmmssFromMinutes(minOfDay) {
const mm = ((minOfDay % 1440) + 1440) % 1440;
const hh = Math.floor(mm / 60);
const mi = mm % 60;
return `${to2(hh)}:${to2(mi)}:00`;
}
function nextDailyOccurrenceMs(startMin, refMs) {
if (!Number.isFinite(startMin)) return null;
const today = getParisParts(refMs).date;
const todayStamp = `${today} ${hhmmssFromMinutes(startMin)}`;
const todayMs = parseParisStampToMs(todayStamp);
if (todayMs !== null && todayMs > refMs + 1000) return todayMs;
const tomorrow = getParisParts(refMs + 36 * 3600 * 1000).date;
const tomorrowStamp = `${tomorrow} ${hhmmssFromMinutes(startMin)}`;
return parseParisStampToMs(tomorrowStamp);
}
const prev = $input.first().json;
const {
nowMs,
cfg,
parsed,
params,
history,
batteryTune,
strategy,
derived,
context,
lifecycle,
} = prev;
// =====================================================
// SESSION / SOURCE
// =====================================================
const sessionObservedActive =
parsed.evcsChargingSwitchOn ||
parsed.controlLatch ||
parsed.chargePermissionNow;
const isGridSource =
context.sourceMode === 'grid_hc_allowed' ||
context.sourceMode === 'grid_forced';
// =====================================================
// SOC BAND / BATTERY DYNAMIC POLICY
// =====================================================
const socBand =
parsed.soc > 90 ? 'gt90' :
parsed.soc > 80 ? '80_90' :
parsed.soc > 60 ? '60_80' :
parsed.soc > 40 ? '40_60' :
parsed.soc > 25 ? '25_40' :
'lt25';
const band = batteryTune.bands[socBand];
const dischargeModeFactor =
strategy.forceChargeRequested ? 1.2 :
strategy.forceGridRequested ? 0.7 :
strategy.userStrategy === 'auto_solar' ? 0.8 :
1.0;
const chargeReserveModeFactor =
strategy.forceChargeRequested ? 0.8 :
strategy.forceGridRequested ? 1.1 :
strategy.userStrategy === 'auto_solar' ? 1.1 :
1.0;
const toleranceModeFactor =
strategy.forceChargeRequested ? 1.25 :
strategy.forceGridRequested ? 0.8 :
strategy.userStrategy === 'auto_solar' ? 0.8 :
1.0;
const batteryDischargeSoftDynamicA = band.dischargeSoftA * dischargeModeFactor;
const batteryDischargeHardDynamicA = band.dischargeHardA * dischargeModeFactor;
const batteryChargeReserveDynamicA = Math.max(0, band.chargeReserveA * chargeReserveModeFactor);
const batterySoftToleranceMinutes = band.softToleranceMin * toleranceModeFactor;
const batteryHardToleranceSeconds = band.hardToleranceSec;
const batteryRecoveryMinutes = band.recoveryMin;
// =====================================================
// CONTEXT HELPERS
// =====================================================
const houseSpikeThresholdW = 1500;
const houseSpikeDetected =
(parsed.houseAcPowerW - parsed.houseAcPowerPrevW) >= houseSpikeThresholdW;
const solarPotentialContext =
context.sourceMode === 'solar_potential' ||
(
parsed.soc >= 80 &&
derived.forecastBufferKwh > 0 &&
parsed.pvPowerW < context.effectivePvStartMinW &&
derived.houseNonEvcsW < 1200
);
// =====================================================
// REASON-BASED LOCKS
// =====================================================
const nextWindowCandidates = [
nextDailyOccurrenceMs(params.morningStartMin, nowMs),
nextDailyOccurrenceMs(params.hcMiddayStartMin, nowMs),
nextDailyOccurrenceMs(params.eveningStartMin, nowMs),
nextDailyOccurrenceMs(params.hcNightStartMin, nowMs),
].filter(v => Number.isFinite(v));
const nextAllowedWindowStartMs =
nextWindowCandidates.length > 0 ? Math.min(...nextWindowCandidates) : null;
const blockActive =
history.blockedUntilMs !== null &&
history.blockedUntilMs > nowMs + 1000;
const blockStillRelevant =
history.lastBlockReason === 'outside_window'
? context.profile === 'outside_window'
: history.lastBlockReason === 'probe_failed'
? context.sourceMode === 'hybrid_probe'
: history.lastBlockReason === 'battery_protection_stop'
? (
parsed.batteryCurrentA < -params.batteryNeutralBandA ||
derived.anyVebusLowBattery ||
derived.vebusDcVoltageSoftLow
)
: history.lastBlockReason === 'grid_battery_guard'
? isGridSource
: history.lastBlockReason === 'vebus_overload_persistent'
? derived.anyVebusOverload
: false;
const startBlockedByReason = blockActive && blockStillRelevant;
// =====================================================
// CLOUD DEBT
// =====================================================
const batteryNetPowerW = parsed.batteryCurrentA * derived.batteryVoltageRef;
let cloudDebtNextKwh = params.cloudDebtPrevKwh;
if (sessionObservedActive) {
if (batteryNetPowerW < 0 && !isGridSource) {
cloudDebtNextKwh += (Math.abs(batteryNetPowerW) * cfg.sampleSeconds) / 3600000;
} else if (batteryNetPowerW > 0) {
cloudDebtNextKwh -= (batteryNetPowerW * cfg.sampleSeconds) / 3600000;
}
} else if (batteryNetPowerW > 0) {
cloudDebtNextKwh -= (batteryNetPowerW * cfg.sampleSeconds) / 3600000;
}
cloudDebtNextKwh = clamp(cloudDebtNextKwh, 0, params.cloudDebtMaxKwh * 4);
const cloudDebtMeaningful =
cloudDebtNextKwh > cfg.cloudDebtMeaningfulThresholdKwh;
const cloudEventDischargingNow =
sessionObservedActive &&
!isGridSource &&
parsed.batteryCurrentA < -Math.max(1, params.batteryNeutralBandA) &&
cloudDebtMeaningful;
const continuingCloudEvent =
cloudEventDischargingNow &&
history.cloudDebtStartedMsPrev !== null &&
params.cloudDebtPrevKwh > cfg.cloudDebtRecoveryThresholdKwh;
const enteringCloudDebt =
cloudEventDischargingNow && !continuingCloudEvent;
const cloudDebtActive = cloudEventDischargingNow;
const cloudDebtStartedMs =
cloudEventDischargingNow
? (continuingCloudEvent ? history.cloudDebtStartedMsPrev : nowMs)
: null;
const cloudDebtDurationMinutes =
cloudDebtActive && cloudDebtStartedMs !== null
? Math.max(0, (nowMs - cloudDebtStartedMs) / 60000)
: 0;
const effectiveCloudToleranceMinutes =
params.cloudToleranceMinutes +
(solarPotentialContext ? batteryTune.global.solarPotentialGraceMinutes : 0) +
(houseSpikeDetected ? batteryTune.global.houseSpikeGraceMinutes : 0);
const cloudToleranceOk =
cloudDebtNextKwh <= params.cloudDebtMaxKwh &&
(
!cloudDebtActive ||
cloudDebtDurationMinutes <= effectiveCloudToleranceMinutes
);