ajout gestion batterie
This commit is contained in:
873
BATTERY/BATTERY_CLOUD_LOCKS.js
Normal file
873
BATTERY/BATTERY_CLOUD_LOCKS.js
Normal file
@@ -0,0 +1,873 @@
|
|||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// LEGACY / FORECAST MODES
|
||||||
|
// =====================================================
|
||||||
|
const batteryFullHarvestMode =
|
||||||
|
parsed.forecastModeEnabled &&
|
||||||
|
context.profile !== 'night_hc' &&
|
||||||
|
context.profile !== 'outside_window' &&
|
||||||
|
parsed.soc >= params.batteryFullSoc &&
|
||||||
|
parsed.todayRemainingKwh >= params.batteryFullForecastMinKwh &&
|
||||||
|
derived.projectedEndSocAfterCharge >= (derived.endDayGuardSoc - 2);
|
||||||
|
|
||||||
|
const forecastCommitMode =
|
||||||
|
parsed.forecastModeEnabled &&
|
||||||
|
context.profile !== 'night_hc' &&
|
||||||
|
context.profile !== 'outside_window' &&
|
||||||
|
parsed.soc >= params.forecastCommitSocFloor &&
|
||||||
|
parsed.todayRemainingKwh >= params.forecastCommitMinKwh &&
|
||||||
|
derived.projectedEndSocAfterCharge >= derived.endDayGuardSoc;
|
||||||
|
|
||||||
|
const batteryPriorityMode =
|
||||||
|
!batteryFullHarvestMode && !forecastCommitMode;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// EVCS NIGHT ENGINE
|
||||||
|
// =====================================================
|
||||||
|
const nightSessionActive =
|
||||||
|
context.profile === 'night_hc' &&
|
||||||
|
parsed.evcsVehicleConnected;
|
||||||
|
|
||||||
|
const nightSessionStartSocPct =
|
||||||
|
nightSessionActive
|
||||||
|
? (
|
||||||
|
Number.isFinite(history.nightSessionStartSocPct)
|
||||||
|
? history.nightSessionStartSocPct
|
||||||
|
: parsed.soc
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const nightSessionStartAt =
|
||||||
|
nightSessionActive
|
||||||
|
? (history.nightSessionStartAt || '')
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const nightSessionStartMs =
|
||||||
|
nightSessionActive
|
||||||
|
? (history.nightSessionStartMs ?? null)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const nightSessionNeedsCapture =
|
||||||
|
nightSessionActive &&
|
||||||
|
(
|
||||||
|
!Number.isFinite(history.nightSessionStartSocPct) ||
|
||||||
|
!nightSessionStartAt ||
|
||||||
|
nightSessionStartMs === null
|
||||||
|
);
|
||||||
|
|
||||||
|
const nightSessionNeedsClear =
|
||||||
|
!nightSessionActive &&
|
||||||
|
(
|
||||||
|
Number.isFinite(history.nightSessionStartSocPct) ||
|
||||||
|
!!history.nightSessionStartAt
|
||||||
|
);
|
||||||
|
|
||||||
|
const batteryHardFloorPct =
|
||||||
|
params.evcsNightBatteryHardFloorPct;
|
||||||
|
|
||||||
|
const batteryMorningTargetPct =
|
||||||
|
parsed.todayRemainingKwh >= params.evcsNightStrongForecastKwh
|
||||||
|
? params.evcsNightTargetStrongForecastPct
|
||||||
|
: parsed.todayRemainingKwh >= params.evcsNightMediumForecastKwh
|
||||||
|
? params.evcsNightTargetMediumForecastPct
|
||||||
|
: params.evcsNightTargetWeakForecastPct;
|
||||||
|
|
||||||
|
const maxBatteryDischargePoints =
|
||||||
|
nightSessionActive && Number.isFinite(nightSessionStartSocPct)
|
||||||
|
? Math.max(0, nightSessionStartSocPct - batteryMorningTargetPct)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const gridTakeoverTriggerPct =
|
||||||
|
Math.max(batteryHardFloorPct, batteryMorningTargetPct);
|
||||||
|
|
||||||
|
const batteryKwhMissingToTarget =
|
||||||
|
nightSessionActive
|
||||||
|
? Math.max(
|
||||||
|
0,
|
||||||
|
((batteryMorningTargetPct - parsed.soc) / 100) * cfg.batteryUsableCapacityKwh
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const batteryRecoveryMinutesNeeded =
|
||||||
|
nightSessionActive && params.evcsNightBatteryRecoveryPowerKw > 0
|
||||||
|
? (batteryKwhMissingToTarget / params.evcsNightBatteryRecoveryPowerKw) * 60
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const batteryRecoveryUrgent =
|
||||||
|
nightSessionActive &&
|
||||||
|
Number.isFinite(lifecycle.minutesUntilDeparture) &&
|
||||||
|
lifecycle.minutesUntilDeparture >= 0 &&
|
||||||
|
(
|
||||||
|
batteryRecoveryMinutesNeeded + params.evcsNightDepartureSafetyMin
|
||||||
|
>= lifecycle.minutesUntilDeparture
|
||||||
|
);
|
||||||
|
|
||||||
|
const batteryBelowHardFloor =
|
||||||
|
parsed.soc <= batteryHardFloorPct;
|
||||||
|
|
||||||
|
const batteryBelowMorningTarget =
|
||||||
|
parsed.soc < batteryMorningTargetPct;
|
||||||
|
|
||||||
|
const batteryAtOrBelowGridTakeoverTrigger =
|
||||||
|
parsed.soc <= gridTakeoverTriggerPct;
|
||||||
|
|
||||||
|
const batteryAssistAllowed =
|
||||||
|
nightSessionActive &&
|
||||||
|
!batteryBelowHardFloor &&
|
||||||
|
!batteryRecoveryUrgent &&
|
||||||
|
parsed.soc > gridTakeoverTriggerPct;
|
||||||
|
|
||||||
|
const nightGridTakeoverNeeded =
|
||||||
|
nightSessionActive &&
|
||||||
|
(
|
||||||
|
batteryBelowHardFloor ||
|
||||||
|
batteryAtOrBelowGridTakeoverTrigger ||
|
||||||
|
batteryRecoveryUrgent
|
||||||
|
);
|
||||||
|
|
||||||
|
const nightPreferredSourceMode =
|
||||||
|
!nightSessionActive
|
||||||
|
? 'blocked'
|
||||||
|
: batteryAssistAllowed
|
||||||
|
? 'battery_assist_night'
|
||||||
|
: (
|
||||||
|
batteryBelowHardFloor ||
|
||||||
|
batteryBelowMorningTarget ||
|
||||||
|
batteryRecoveryUrgent
|
||||||
|
)
|
||||||
|
? 'grid_forced'
|
||||||
|
: 'grid_hc_allowed';
|
||||||
|
|
||||||
|
const batteryRecoveryAllowedNow =
|
||||||
|
nightSessionActive &&
|
||||||
|
nightGridTakeoverNeeded &&
|
||||||
|
(
|
||||||
|
batteryBelowMorningTarget ||
|
||||||
|
batteryRecoveryUrgent
|
||||||
|
);
|
||||||
|
|
||||||
|
const batteryTargetReached =
|
||||||
|
nightSessionActive &&
|
||||||
|
parsed.soc >= batteryMorningTargetPct;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// PROFILE-BASED THRESHOLDS
|
||||||
|
// =====================================================
|
||||||
|
const profileSocStartFloor =
|
||||||
|
context.profile === 'morning_solar' ? params.morningSocMin :
|
||||||
|
context.profile === 'midday_hc_or_solar' ? params.middaySocMin :
|
||||||
|
context.profile === 'evening_high_soc' ? params.eveningSocStartMin :
|
||||||
|
params.socStartMin;
|
||||||
|
|
||||||
|
const profileSocHoldFloor =
|
||||||
|
context.profile === 'morning_solar' ? Math.max(params.socStopMin, params.morningSocMin - 10) :
|
||||||
|
context.profile === 'midday_hc_or_solar' ? Math.max(params.socStopMin, params.middaySocMin - 15) :
|
||||||
|
context.profile === 'evening_high_soc' ? params.eveningSocStopMin :
|
||||||
|
params.socStopMin;
|
||||||
|
|
||||||
|
const profileSpecificDischargeFloorA =
|
||||||
|
context.profile === 'morning_solar' ? params.morningBatteryMaxDischargeA :
|
||||||
|
context.profile === 'midday_hc_or_solar' ? params.middayBatteryMaxDischargeA :
|
||||||
|
context.profile === 'evening_high_soc' ? params.eveningBatteryMaxDischargeA :
|
||||||
|
params.batteryDischargeSoftA;
|
||||||
|
|
||||||
|
const profileBatteryDischargeFloorA =
|
||||||
|
Math.max(profileSpecificDischargeFloorA, batteryDischargeSoftDynamicA);
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// GRID RELAX IN force_charge_now
|
||||||
|
// =====================================================
|
||||||
|
// Assouplissement uniquement pour la garde réseau,
|
||||||
|
// sans toucher aux sécurités dures globales.
|
||||||
|
const gridForceChargeRelaxSoftA =
|
||||||
|
strategy.forceChargeRequested && isGridSource
|
||||||
|
? (
|
||||||
|
parsed.soc >= 90 ? 28 :
|
||||||
|
parsed.soc >= 80 ? 16 :
|
||||||
|
parsed.soc >= 60 ? 8 :
|
||||||
|
parsed.soc >= 40 ? 4 :
|
||||||
|
0
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const gridForceChargeRelaxHardA =
|
||||||
|
strategy.forceChargeRequested && isGridSource
|
||||||
|
? (
|
||||||
|
parsed.soc >= 90 ? 39 :
|
||||||
|
parsed.soc >= 80 ? 29 :
|
||||||
|
parsed.soc >= 60 ? 18 :
|
||||||
|
parsed.soc >= 40 ? 8 :
|
||||||
|
0
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const gridBatterySoftDynamicABase =
|
||||||
|
Math.max(params.gridBatterySoftA, batteryDischargeSoftDynamicA);
|
||||||
|
|
||||||
|
const gridBatteryHardDynamicABase =
|
||||||
|
Math.max(params.gridBatteryHardA, batteryDischargeHardDynamicA);
|
||||||
|
|
||||||
|
let gridBatterySoftDynamicA =
|
||||||
|
gridBatterySoftDynamicABase - gridForceChargeRelaxSoftA;
|
||||||
|
|
||||||
|
let gridBatteryHardDynamicA =
|
||||||
|
gridBatteryHardDynamicABase - gridForceChargeRelaxHardA;
|
||||||
|
|
||||||
|
// Garantit que le hard reste plus permissif que le soft d'au moins 3 A
|
||||||
|
if (gridBatteryHardDynamicA > (gridBatterySoftDynamicA - 3)) {
|
||||||
|
gridBatteryHardDynamicA = gridBatterySoftDynamicA - 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// BASE SAFETY
|
||||||
|
// =====================================================
|
||||||
|
const commonSafetyStartOk =
|
||||||
|
derived.canRunAtMinCurrent &&
|
||||||
|
!derived.anyVebusOverload &&
|
||||||
|
!derived.anyVebusLowBattery &&
|
||||||
|
!derived.vebusDcVoltageSoftLow &&
|
||||||
|
!derived.vebusAcOutSoftReached &&
|
||||||
|
parsed.evcsDeviceConnected &&
|
||||||
|
parsed.evcsVehicleConnected &&
|
||||||
|
!parsed.evcsModeIsAuto;
|
||||||
|
|
||||||
|
const commonSafetyHoldOk =
|
||||||
|
derived.canRunAtMinCurrent &&
|
||||||
|
!derived.anyVebusLowBattery &&
|
||||||
|
!derived.vebusDcVoltageHardLow &&
|
||||||
|
parsed.evcsDeviceConnected &&
|
||||||
|
parsed.evcsVehicleConnected &&
|
||||||
|
!parsed.evcsModeIsAuto;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// START / HOLD CONDITIONS
|
||||||
|
// =====================================================
|
||||||
|
const solarStartOk =
|
||||||
|
(context.sourceMode === 'solar_only' || context.sourceMode === 'solar_potential') &&
|
||||||
|
commonSafetyStartOk &&
|
||||||
|
parsed.soc >= profileSocStartFloor &&
|
||||||
|
parsed.batteryCurrentA >= profileBatteryDischargeFloorA &&
|
||||||
|
(
|
||||||
|
context.sourceMode === 'solar_potential' ||
|
||||||
|
parsed.pvPowerW >= context.effectivePvStartMinW
|
||||||
|
);
|
||||||
|
|
||||||
|
const solarHoldOk =
|
||||||
|
(context.sourceMode === 'solar_only' || context.sourceMode === 'solar_potential') &&
|
||||||
|
commonSafetyHoldOk &&
|
||||||
|
parsed.soc >= profileSocHoldFloor &&
|
||||||
|
parsed.batteryCurrentA >= profileBatteryDischargeFloorA &&
|
||||||
|
(
|
||||||
|
parsed.pvPowerW >= params.pvHoldMinW ||
|
||||||
|
cloudToleranceOk
|
||||||
|
);
|
||||||
|
|
||||||
|
const gridStartOk =
|
||||||
|
isGridSource &&
|
||||||
|
commonSafetyStartOk &&
|
||||||
|
parsed.gridAvailable &&
|
||||||
|
parsed.batteryCurrentA >= gridBatterySoftDynamicA;
|
||||||
|
|
||||||
|
const gridHoldOk =
|
||||||
|
isGridSource &&
|
||||||
|
commonSafetyHoldOk &&
|
||||||
|
parsed.gridAvailable &&
|
||||||
|
parsed.batteryCurrentA >= gridBatteryHardDynamicA;
|
||||||
|
|
||||||
|
const batteryAssistNightStartOk =
|
||||||
|
context.sourceMode === 'battery_assist_night' &&
|
||||||
|
commonSafetyStartOk &&
|
||||||
|
nightSessionActive &&
|
||||||
|
batteryAssistAllowed &&
|
||||||
|
parsed.soc > batteryHardFloorPct &&
|
||||||
|
parsed.soc > gridTakeoverTriggerPct &&
|
||||||
|
parsed.batteryCurrentA >= profileBatteryDischargeFloorA;
|
||||||
|
|
||||||
|
const batteryAssistNightHoldOk =
|
||||||
|
context.sourceMode === 'battery_assist_night' &&
|
||||||
|
commonSafetyHoldOk &&
|
||||||
|
nightSessionActive &&
|
||||||
|
batteryAssistAllowed &&
|
||||||
|
parsed.soc > batteryHardFloorPct &&
|
||||||
|
parsed.soc > gridTakeoverTriggerPct &&
|
||||||
|
parsed.batteryCurrentA >= batteryDischargeHardDynamicA;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// PROBE LOGIC — STRICTEMENT LIMITÉE AU MODE PROBE
|
||||||
|
// =====================================================
|
||||||
|
const probeModeActive = context.sourceMode === 'hybrid_probe';
|
||||||
|
|
||||||
|
const probeStartOk =
|
||||||
|
probeModeActive &&
|
||||||
|
commonSafetyStartOk &&
|
||||||
|
(context.morningProbeEligible || context.middayProbeEligible);
|
||||||
|
|
||||||
|
const probeHoldGuardOk =
|
||||||
|
probeModeActive &&
|
||||||
|
commonSafetyHoldOk &&
|
||||||
|
parsed.batteryCurrentA >= profileBatteryDischargeFloorA;
|
||||||
|
|
||||||
|
const probeWindowActive =
|
||||||
|
probeModeActive &&
|
||||||
|
sessionObservedActive &&
|
||||||
|
history.probeStartedMs !== null &&
|
||||||
|
lifecycle.minutesSinceProbeStart !== null &&
|
||||||
|
lifecycle.minutesSinceProbeStart < params.probeMinutes;
|
||||||
|
|
||||||
|
const probeElapsedReached =
|
||||||
|
probeModeActive &&
|
||||||
|
sessionObservedActive &&
|
||||||
|
history.probeStartedMs !== null &&
|
||||||
|
lifecycle.minutesSinceProbeStart !== null &&
|
||||||
|
lifecycle.minutesSinceProbeStart >= params.probeMinutes;
|
||||||
|
|
||||||
|
const probeSuccessNow =
|
||||||
|
probeModeActive &&
|
||||||
|
sessionObservedActive &&
|
||||||
|
history.probeStartedMs !== null &&
|
||||||
|
(
|
||||||
|
parsed.pvPowerW >= params.pvHoldMinW &&
|
||||||
|
parsed.batteryCurrentA >= Math.min(0, batteryDischargeSoftDynamicA)
|
||||||
|
);
|
||||||
|
|
||||||
|
const probeFailedNow =
|
||||||
|
probeModeActive &&
|
||||||
|
probeElapsedReached &&
|
||||||
|
!probeSuccessNow;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// STEP-UP LOGIC
|
||||||
|
// =====================================================
|
||||||
|
const stepUpByBatteryReserve =
|
||||||
|
batteryPriorityMode &&
|
||||||
|
parsed.batteryCurrentA >= batteryChargeReserveDynamicA;
|
||||||
|
|
||||||
|
const stepUpByForecastMode =
|
||||||
|
!batteryPriorityMode &&
|
||||||
|
(
|
||||||
|
parsed.pvPowerW >= params.pvHoldMinW ||
|
||||||
|
parsed.pvPowerAvg10mW >= params.pvHoldMinW
|
||||||
|
) &&
|
||||||
|
derived.forecastBufferKwh >= -1 &&
|
||||||
|
derived.projectedEndSocAfterCharge >= (derived.endDayGuardSoc - 5) &&
|
||||||
|
cloudToleranceOk;
|
||||||
|
|
||||||
|
const solarShouldStepUp =
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA < derived.physicalCapA &&
|
||||||
|
derived.physicalCapA >= params.currentMinA &&
|
||||||
|
!derived.vebusAcOutSoftReached &&
|
||||||
|
!derived.vebusDcVoltageSoftLow &&
|
||||||
|
!derived.houseLoadHighVsHeadroom &&
|
||||||
|
(
|
||||||
|
stepUpByBatteryReserve ||
|
||||||
|
stepUpByForecastMode ||
|
||||||
|
parsed.pvPowerW >= (context.effectivePvStartMinW + 400)
|
||||||
|
);
|
||||||
|
|
||||||
|
const gridShouldStepUp =
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA < derived.physicalCapA &&
|
||||||
|
derived.physicalCapA >= params.currentMinA &&
|
||||||
|
!derived.vebusAcOutSoftReached &&
|
||||||
|
!derived.vebusDcVoltageSoftLow &&
|
||||||
|
parsed.batteryCurrentA >= gridBatterySoftDynamicA &&
|
||||||
|
!derived.houseLoadHighVsHeadroom;
|
||||||
|
|
||||||
|
const batteryAssistNightShouldStepUp =
|
||||||
|
context.sourceMode === 'battery_assist_night' &&
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA < derived.physicalCapA &&
|
||||||
|
derived.physicalCapA >= params.currentMinA &&
|
||||||
|
!derived.vebusAcOutSoftReached &&
|
||||||
|
!derived.vebusDcVoltageSoftLow &&
|
||||||
|
!derived.houseLoadHighVsHeadroom &&
|
||||||
|
parsed.batteryCurrentA >= batteryChargeReserveDynamicA &&
|
||||||
|
batteryAssistAllowed;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// STEP-DOWN LOGIC
|
||||||
|
// =====================================================
|
||||||
|
const solarShouldStepDownBattery =
|
||||||
|
parsed.batteryCurrentA <= batteryDischargeSoftDynamicA &&
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA > params.currentMinA;
|
||||||
|
|
||||||
|
const solarShouldStepDownCloudDebt =
|
||||||
|
!cloudToleranceOk &&
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA > params.currentMinA;
|
||||||
|
|
||||||
|
const gridShouldStepDownBattery =
|
||||||
|
parsed.batteryCurrentA < gridBatterySoftDynamicA &&
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA > params.currentMinA;
|
||||||
|
|
||||||
|
const batteryAssistNightShouldStepDown =
|
||||||
|
context.sourceMode === 'battery_assist_night' &&
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA > params.currentMinA &&
|
||||||
|
(
|
||||||
|
parsed.batteryCurrentA <= batteryDischargeSoftDynamicA ||
|
||||||
|
parsed.soc <= gridTakeoverTriggerPct ||
|
||||||
|
batteryRecoveryUrgent
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldStepDownVebusPower =
|
||||||
|
derived.vebusAcOutSoftReached &&
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA > params.currentMinA;
|
||||||
|
|
||||||
|
const shouldStepDownVebusVoltage =
|
||||||
|
derived.vebusDcVoltageSoftLow &&
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA > params.currentMinA;
|
||||||
|
|
||||||
|
const shouldStepDownOverload =
|
||||||
|
derived.anyVebusOverload &&
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA > params.currentMinA;
|
||||||
|
|
||||||
|
const shouldStopOverloadAtMin =
|
||||||
|
derived.anyVebusOverload &&
|
||||||
|
derived.currentReferenceA <= params.currentMinA &&
|
||||||
|
sessionObservedActive;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// HARD STOPS / PROTECTIONS
|
||||||
|
// =====================================================
|
||||||
|
const batteryCurrentHardExceeded =
|
||||||
|
parsed.batteryCurrentA <= batteryDischargeHardDynamicA;
|
||||||
|
|
||||||
|
// overload volontairement exclu du hard stop immédiat
|
||||||
|
const mustStopHard =
|
||||||
|
derived.anyVebusLowBattery ||
|
||||||
|
derived.vebusDcVoltageHardLow ||
|
||||||
|
derived.vebusAcOutHardReached ||
|
||||||
|
batteryCurrentHardExceeded;
|
||||||
|
|
||||||
|
const gridBatteryProtectionStop =
|
||||||
|
sessionObservedActive &&
|
||||||
|
isGridSource &&
|
||||||
|
parsed.batteryCurrentA < gridBatteryHardDynamicA;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// OUTPUT
|
||||||
|
// =====================================================
|
||||||
|
return [{
|
||||||
|
json: {
|
||||||
|
...prev,
|
||||||
|
|
||||||
|
batteryPolicy: {
|
||||||
|
socBand,
|
||||||
|
batteryDischargeSoftDynamicA,
|
||||||
|
batteryDischargeHardDynamicA,
|
||||||
|
batteryChargeReserveDynamicA,
|
||||||
|
batterySoftToleranceMinutes,
|
||||||
|
batteryHardToleranceSeconds,
|
||||||
|
batteryRecoveryMinutes,
|
||||||
|
|
||||||
|
gridBatterySoftDynamicA,
|
||||||
|
gridBatteryHardDynamicA,
|
||||||
|
gridForceChargeRelaxSoftA,
|
||||||
|
gridForceChargeRelaxHardA,
|
||||||
|
|
||||||
|
houseSpikeDetected,
|
||||||
|
solarPotentialContext,
|
||||||
|
},
|
||||||
|
|
||||||
|
locks: {
|
||||||
|
nextAllowedWindowStartMs,
|
||||||
|
blockActive,
|
||||||
|
blockStillRelevant,
|
||||||
|
startBlockedByReason,
|
||||||
|
},
|
||||||
|
|
||||||
|
cloud: {
|
||||||
|
sessionObservedActive,
|
||||||
|
batteryNetPowerW,
|
||||||
|
cloudDebtNextKwh,
|
||||||
|
cloudDebtMeaningful,
|
||||||
|
cloudEventDischargingNow,
|
||||||
|
continuingCloudEvent,
|
||||||
|
enteringCloudDebt,
|
||||||
|
cloudDebtActive,
|
||||||
|
cloudDebtStartedMs,
|
||||||
|
cloudDebtDurationMinutes,
|
||||||
|
effectiveCloudToleranceMinutes,
|
||||||
|
cloudToleranceOk,
|
||||||
|
},
|
||||||
|
|
||||||
|
modes: {
|
||||||
|
batteryFullHarvestMode,
|
||||||
|
forecastCommitMode,
|
||||||
|
batteryPriorityMode,
|
||||||
|
},
|
||||||
|
|
||||||
|
nightEvcs: {
|
||||||
|
nightSessionActive,
|
||||||
|
nightSessionNeedsCapture,
|
||||||
|
nightSessionNeedsClear,
|
||||||
|
nightSessionStartSocPct,
|
||||||
|
nightSessionStartAt,
|
||||||
|
nightSessionStartMs,
|
||||||
|
|
||||||
|
batteryHardFloorPct,
|
||||||
|
batteryMorningTargetPct,
|
||||||
|
maxBatteryDischargePoints,
|
||||||
|
gridTakeoverTriggerPct,
|
||||||
|
|
||||||
|
batteryBelowHardFloor,
|
||||||
|
batteryBelowMorningTarget,
|
||||||
|
batteryAtOrBelowGridTakeoverTrigger,
|
||||||
|
|
||||||
|
batteryKwhMissingToTarget,
|
||||||
|
batteryRecoveryMinutesNeeded,
|
||||||
|
batteryRecoveryUrgent,
|
||||||
|
batteryRecoveryAllowedNow,
|
||||||
|
batteryTargetReached,
|
||||||
|
|
||||||
|
batteryAssistAllowed,
|
||||||
|
nightGridTakeoverNeeded,
|
||||||
|
nightPreferredSourceMode,
|
||||||
|
},
|
||||||
|
|
||||||
|
safeguards: {
|
||||||
|
isGridSource,
|
||||||
|
commonSafetyStartOk,
|
||||||
|
commonSafetyHoldOk,
|
||||||
|
profileSocStartFloor,
|
||||||
|
profileSocHoldFloor,
|
||||||
|
profileBatteryDischargeFloorA,
|
||||||
|
batteryCurrentHardExceeded,
|
||||||
|
mustStopHard,
|
||||||
|
gridBatteryProtectionStop,
|
||||||
|
},
|
||||||
|
|
||||||
|
conditions: {
|
||||||
|
solarStartOk,
|
||||||
|
solarHoldOk,
|
||||||
|
gridStartOk,
|
||||||
|
gridHoldOk,
|
||||||
|
|
||||||
|
batteryAssistNightStartOk,
|
||||||
|
batteryAssistNightHoldOk,
|
||||||
|
|
||||||
|
probeStartOk,
|
||||||
|
probeHoldGuardOk,
|
||||||
|
probeWindowActive,
|
||||||
|
probeElapsedReached,
|
||||||
|
probeSuccessNow,
|
||||||
|
probeFailedNow,
|
||||||
|
|
||||||
|
solarShouldStepUp,
|
||||||
|
gridShouldStepUp,
|
||||||
|
batteryAssistNightShouldStepUp,
|
||||||
|
|
||||||
|
solarShouldStepDownBattery,
|
||||||
|
solarShouldStepDownCloudDebt,
|
||||||
|
gridShouldStepDownBattery,
|
||||||
|
batteryAssistNightShouldStepDown,
|
||||||
|
|
||||||
|
shouldStepDownVebusPower,
|
||||||
|
shouldStepDownVebusVoltage,
|
||||||
|
shouldStepDownOverload,
|
||||||
|
shouldStopOverloadAtMin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
|
||||||
235
BATTERY/BATTERY_CLOUD_LOCKSP1.js
Normal file
235
BATTERY/BATTERY_CLOUD_LOCKSP1.js
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
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;
|
||||||
233
BATTERY/BATTERY_CLOUD_LOCKSP2.js
Normal file
233
BATTERY/BATTERY_CLOUD_LOCKSP2.js
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
// =====================================================
|
||||||
|
// 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
|
||||||
|
);
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// LEGACY / FORECAST MODES
|
||||||
|
// =====================================================
|
||||||
|
const batteryFullHarvestMode =
|
||||||
|
parsed.forecastModeEnabled &&
|
||||||
|
context.profile !== 'night_hc' &&
|
||||||
|
context.profile !== 'outside_window' &&
|
||||||
|
parsed.soc >= params.batteryFullSoc &&
|
||||||
|
parsed.todayRemainingKwh >= params.batteryFullForecastMinKwh &&
|
||||||
|
derived.projectedEndSocAfterCharge >= (derived.endDayGuardSoc - 2);
|
||||||
|
|
||||||
|
const forecastCommitMode =
|
||||||
|
parsed.forecastModeEnabled &&
|
||||||
|
context.profile !== 'night_hc' &&
|
||||||
|
context.profile !== 'outside_window' &&
|
||||||
|
parsed.soc >= params.forecastCommitSocFloor &&
|
||||||
|
parsed.todayRemainingKwh >= params.forecastCommitMinKwh &&
|
||||||
|
derived.projectedEndSocAfterCharge >= derived.endDayGuardSoc;
|
||||||
|
|
||||||
|
const batteryPriorityMode =
|
||||||
|
!batteryFullHarvestMode && !forecastCommitMode;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// EVCS NIGHT ENGINE
|
||||||
|
// =====================================================
|
||||||
|
const nightSessionActive =
|
||||||
|
context.profile === 'night_hc' &&
|
||||||
|
parsed.evcsVehicleConnected;
|
||||||
|
|
||||||
|
const nightSessionStartSocPct =
|
||||||
|
nightSessionActive
|
||||||
|
? (
|
||||||
|
Number.isFinite(history.nightSessionStartSocPct)
|
||||||
|
? history.nightSessionStartSocPct
|
||||||
|
: parsed.soc
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const nightSessionStartAt =
|
||||||
|
nightSessionActive
|
||||||
|
? (history.nightSessionStartAt || '')
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const nightSessionStartMs =
|
||||||
|
nightSessionActive
|
||||||
|
? (history.nightSessionStartMs ?? null)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const nightSessionNeedsCapture =
|
||||||
|
nightSessionActive &&
|
||||||
|
(
|
||||||
|
!Number.isFinite(history.nightSessionStartSocPct) ||
|
||||||
|
!nightSessionStartAt ||
|
||||||
|
nightSessionStartMs === null
|
||||||
|
);
|
||||||
|
|
||||||
|
const nightSessionNeedsClear =
|
||||||
|
!nightSessionActive &&
|
||||||
|
(
|
||||||
|
Number.isFinite(history.nightSessionStartSocPct) ||
|
||||||
|
!!history.nightSessionStartAt
|
||||||
|
);
|
||||||
|
|
||||||
|
const batteryHardFloorPct =
|
||||||
|
params.evcsNightBatteryHardFloorPct;
|
||||||
|
|
||||||
|
const batteryMorningTargetPct =
|
||||||
|
parsed.todayRemainingKwh >= params.evcsNightStrongForecastKwh
|
||||||
|
? params.evcsNightTargetStrongForecastPct
|
||||||
|
: parsed.todayRemainingKwh >= params.evcsNightMediumForecastKwh
|
||||||
|
? params.evcsNightTargetMediumForecastPct
|
||||||
|
: params.evcsNightTargetWeakForecastPct;
|
||||||
|
|
||||||
|
const maxBatteryDischargePoints =
|
||||||
|
nightSessionActive && Number.isFinite(nightSessionStartSocPct)
|
||||||
|
? Math.max(0, nightSessionStartSocPct - batteryMorningTargetPct)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const gridTakeoverTriggerPct =
|
||||||
|
Math.max(batteryHardFloorPct, batteryMorningTargetPct);
|
||||||
|
|
||||||
|
const batteryKwhMissingToTarget =
|
||||||
|
nightSessionActive
|
||||||
|
? Math.max(
|
||||||
|
0,
|
||||||
|
((batteryMorningTargetPct - parsed.soc) / 100) * cfg.batteryUsableCapacityKwh
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const batteryRecoveryMinutesNeeded =
|
||||||
|
nightSessionActive && params.evcsNightBatteryRecoveryPowerKw > 0
|
||||||
|
? (batteryKwhMissingToTarget / params.evcsNightBatteryRecoveryPowerKw) * 60
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const batteryRecoveryUrgent =
|
||||||
|
nightSessionActive &&
|
||||||
|
Number.isFinite(lifecycle.minutesUntilDeparture) &&
|
||||||
|
lifecycle.minutesUntilDeparture >= 0 &&
|
||||||
|
(
|
||||||
|
batteryRecoveryMinutesNeeded + params.evcsNightDepartureSafetyMin
|
||||||
|
>= lifecycle.minutesUntilDeparture
|
||||||
|
);
|
||||||
|
|
||||||
|
const batteryBelowHardFloor =
|
||||||
|
parsed.soc <= batteryHardFloorPct;
|
||||||
|
|
||||||
|
const batteryBelowMorningTarget =
|
||||||
|
parsed.soc < batteryMorningTargetPct;
|
||||||
|
|
||||||
|
const batteryAtOrBelowGridTakeoverTrigger =
|
||||||
|
parsed.soc <= gridTakeoverTriggerPct;
|
||||||
|
|
||||||
|
const batteryAssistAllowed =
|
||||||
|
nightSessionActive &&
|
||||||
|
!batteryBelowHardFloor &&
|
||||||
|
!batteryRecoveryUrgent &&
|
||||||
|
parsed.soc > gridTakeoverTriggerPct;
|
||||||
|
|
||||||
|
const nightGridTakeoverNeeded =
|
||||||
|
nightSessionActive &&
|
||||||
|
(
|
||||||
|
batteryBelowHardFloor ||
|
||||||
|
batteryAtOrBelowGridTakeoverTrigger ||
|
||||||
|
batteryRecoveryUrgent
|
||||||
|
);
|
||||||
|
|
||||||
|
const nightPreferredSourceMode =
|
||||||
|
!nightSessionActive
|
||||||
|
? 'blocked'
|
||||||
|
: batteryAssistAllowed
|
||||||
|
? 'battery_assist_night'
|
||||||
|
: (
|
||||||
|
batteryBelowHardFloor ||
|
||||||
|
batteryBelowMorningTarget ||
|
||||||
|
batteryRecoveryUrgent
|
||||||
|
)
|
||||||
|
? 'grid_forced'
|
||||||
|
: 'grid_hc_allowed';
|
||||||
|
|
||||||
|
const batteryRecoveryAllowedNow =
|
||||||
|
nightSessionActive &&
|
||||||
|
nightGridTakeoverNeeded &&
|
||||||
|
(
|
||||||
|
batteryBelowMorningTarget ||
|
||||||
|
batteryRecoveryUrgent
|
||||||
|
);
|
||||||
|
|
||||||
|
const batteryTargetReached =
|
||||||
|
nightSessionActive &&
|
||||||
|
parsed.soc >= batteryMorningTargetPct;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// PROFILE-BASED THRESHOLDS
|
||||||
|
// =====================================================
|
||||||
|
const profileSocStartFloor =
|
||||||
|
context.profile === 'morning_solar' ? params.morningSocMin :
|
||||||
|
context.profile === 'midday_hc_or_solar' ? params.middaySocMin :
|
||||||
|
context.profile === 'evening_high_soc' ? params.eveningSocStartMin :
|
||||||
|
params.socStartMin;
|
||||||
|
|
||||||
|
const profileSocHoldFloor =
|
||||||
|
context.profile === 'morning_solar' ? Math.max(params.socStopMin, params.morningSocMin - 10) :
|
||||||
|
context.profile === 'midday_hc_or_solar' ? Math.max(params.socStopMin, params.middaySocMin - 15) :
|
||||||
|
context.profile === 'evening_high_soc' ? params.eveningSocStopMin :
|
||||||
|
params.socStopMin;
|
||||||
|
|
||||||
|
const profileSpecificDischargeFloorA =
|
||||||
|
context.profile === 'morning_solar' ? params.morningBatteryMaxDischargeA :
|
||||||
|
context.profile === 'midday_hc_or_solar' ? params.middayBatteryMaxDischargeA :
|
||||||
|
context.profile === 'evening_high_soc' ? params.eveningBatteryMaxDischargeA :
|
||||||
|
params.batteryDischargeSoftA;
|
||||||
|
|
||||||
|
const profileBatteryDischargeFloorA =
|
||||||
|
Math.max(profileSpecificDischargeFloorA, batteryDischargeSoftDynamicA);
|
||||||
404
BATTERY/BATTERY_CLOUD_LOCKSP3.js
Normal file
404
BATTERY/BATTERY_CLOUD_LOCKSP3.js
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// GRID RELAX IN force_charge_now
|
||||||
|
// =====================================================
|
||||||
|
// Assouplissement uniquement pour la garde réseau,
|
||||||
|
// sans toucher aux sécurités dures globales.
|
||||||
|
const gridForceChargeRelaxSoftA =
|
||||||
|
strategy.forceChargeRequested && isGridSource
|
||||||
|
? (
|
||||||
|
parsed.soc >= 90 ? 28 :
|
||||||
|
parsed.soc >= 80 ? 16 :
|
||||||
|
parsed.soc >= 60 ? 8 :
|
||||||
|
parsed.soc >= 40 ? 4 :
|
||||||
|
0
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const gridForceChargeRelaxHardA =
|
||||||
|
strategy.forceChargeRequested && isGridSource
|
||||||
|
? (
|
||||||
|
parsed.soc >= 90 ? 39 :
|
||||||
|
parsed.soc >= 80 ? 29 :
|
||||||
|
parsed.soc >= 60 ? 18 :
|
||||||
|
parsed.soc >= 40 ? 8 :
|
||||||
|
0
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const gridBatterySoftDynamicABase =
|
||||||
|
Math.max(params.gridBatterySoftA, batteryDischargeSoftDynamicA);
|
||||||
|
|
||||||
|
const gridBatteryHardDynamicABase =
|
||||||
|
Math.max(params.gridBatteryHardA, batteryDischargeHardDynamicA);
|
||||||
|
|
||||||
|
let gridBatterySoftDynamicA =
|
||||||
|
gridBatterySoftDynamicABase - gridForceChargeRelaxSoftA;
|
||||||
|
|
||||||
|
let gridBatteryHardDynamicA =
|
||||||
|
gridBatteryHardDynamicABase - gridForceChargeRelaxHardA;
|
||||||
|
|
||||||
|
// Garantit que le hard reste plus permissif que le soft d'au moins 3 A
|
||||||
|
if (gridBatteryHardDynamicA > (gridBatterySoftDynamicA - 3)) {
|
||||||
|
gridBatteryHardDynamicA = gridBatterySoftDynamicA - 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// BASE SAFETY
|
||||||
|
// =====================================================
|
||||||
|
const commonSafetyStartOk =
|
||||||
|
derived.canRunAtMinCurrent &&
|
||||||
|
!derived.anyVebusOverload &&
|
||||||
|
!derived.anyVebusLowBattery &&
|
||||||
|
!derived.vebusDcVoltageSoftLow &&
|
||||||
|
!derived.vebusAcOutSoftReached &&
|
||||||
|
parsed.evcsDeviceConnected &&
|
||||||
|
parsed.evcsVehicleConnected &&
|
||||||
|
!parsed.evcsModeIsAuto;
|
||||||
|
|
||||||
|
const commonSafetyHoldOk =
|
||||||
|
derived.canRunAtMinCurrent &&
|
||||||
|
!derived.anyVebusLowBattery &&
|
||||||
|
!derived.vebusDcVoltageHardLow &&
|
||||||
|
parsed.evcsDeviceConnected &&
|
||||||
|
parsed.evcsVehicleConnected &&
|
||||||
|
!parsed.evcsModeIsAuto;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// START / HOLD CONDITIONS
|
||||||
|
// =====================================================
|
||||||
|
const solarStartOk =
|
||||||
|
(context.sourceMode === 'solar_only' || context.sourceMode === 'solar_potential') &&
|
||||||
|
commonSafetyStartOk &&
|
||||||
|
parsed.soc >= profileSocStartFloor &&
|
||||||
|
parsed.batteryCurrentA >= profileBatteryDischargeFloorA &&
|
||||||
|
(
|
||||||
|
context.sourceMode === 'solar_potential' ||
|
||||||
|
parsed.pvPowerW >= context.effectivePvStartMinW
|
||||||
|
);
|
||||||
|
|
||||||
|
const solarHoldOk =
|
||||||
|
(context.sourceMode === 'solar_only' || context.sourceMode === 'solar_potential') &&
|
||||||
|
commonSafetyHoldOk &&
|
||||||
|
parsed.soc >= profileSocHoldFloor &&
|
||||||
|
parsed.batteryCurrentA >= profileBatteryDischargeFloorA &&
|
||||||
|
(
|
||||||
|
parsed.pvPowerW >= params.pvHoldMinW ||
|
||||||
|
cloudToleranceOk
|
||||||
|
);
|
||||||
|
|
||||||
|
const gridStartOk =
|
||||||
|
isGridSource &&
|
||||||
|
commonSafetyStartOk &&
|
||||||
|
parsed.gridAvailable &&
|
||||||
|
parsed.batteryCurrentA >= gridBatterySoftDynamicA;
|
||||||
|
|
||||||
|
const gridHoldOk =
|
||||||
|
isGridSource &&
|
||||||
|
commonSafetyHoldOk &&
|
||||||
|
parsed.gridAvailable &&
|
||||||
|
parsed.batteryCurrentA >= gridBatteryHardDynamicA;
|
||||||
|
|
||||||
|
const batteryAssistNightStartOk =
|
||||||
|
context.sourceMode === 'battery_assist_night' &&
|
||||||
|
commonSafetyStartOk &&
|
||||||
|
nightSessionActive &&
|
||||||
|
batteryAssistAllowed &&
|
||||||
|
parsed.soc > batteryHardFloorPct &&
|
||||||
|
parsed.soc > gridTakeoverTriggerPct &&
|
||||||
|
parsed.batteryCurrentA >= profileBatteryDischargeFloorA;
|
||||||
|
|
||||||
|
const batteryAssistNightHoldOk =
|
||||||
|
context.sourceMode === 'battery_assist_night' &&
|
||||||
|
commonSafetyHoldOk &&
|
||||||
|
nightSessionActive &&
|
||||||
|
batteryAssistAllowed &&
|
||||||
|
parsed.soc > batteryHardFloorPct &&
|
||||||
|
parsed.soc > gridTakeoverTriggerPct &&
|
||||||
|
parsed.batteryCurrentA >= batteryDischargeHardDynamicA;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// PROBE LOGIC — STRICTEMENT LIMITÉE AU MODE PROBE
|
||||||
|
// =====================================================
|
||||||
|
const probeModeActive = context.sourceMode === 'hybrid_probe';
|
||||||
|
|
||||||
|
const probeStartOk =
|
||||||
|
probeModeActive &&
|
||||||
|
commonSafetyStartOk &&
|
||||||
|
(context.morningProbeEligible || context.middayProbeEligible);
|
||||||
|
|
||||||
|
const probeHoldGuardOk =
|
||||||
|
probeModeActive &&
|
||||||
|
commonSafetyHoldOk &&
|
||||||
|
parsed.batteryCurrentA >= profileBatteryDischargeFloorA;
|
||||||
|
|
||||||
|
const probeWindowActive =
|
||||||
|
probeModeActive &&
|
||||||
|
sessionObservedActive &&
|
||||||
|
history.probeStartedMs !== null &&
|
||||||
|
lifecycle.minutesSinceProbeStart !== null &&
|
||||||
|
lifecycle.minutesSinceProbeStart < params.probeMinutes;
|
||||||
|
|
||||||
|
const probeElapsedReached =
|
||||||
|
probeModeActive &&
|
||||||
|
sessionObservedActive &&
|
||||||
|
history.probeStartedMs !== null &&
|
||||||
|
lifecycle.minutesSinceProbeStart !== null &&
|
||||||
|
lifecycle.minutesSinceProbeStart >= params.probeMinutes;
|
||||||
|
|
||||||
|
const probeSuccessNow =
|
||||||
|
probeModeActive &&
|
||||||
|
sessionObservedActive &&
|
||||||
|
history.probeStartedMs !== null &&
|
||||||
|
(
|
||||||
|
parsed.pvPowerW >= params.pvHoldMinW &&
|
||||||
|
parsed.batteryCurrentA >= Math.min(0, batteryDischargeSoftDynamicA)
|
||||||
|
);
|
||||||
|
|
||||||
|
const probeFailedNow =
|
||||||
|
probeModeActive &&
|
||||||
|
probeElapsedReached &&
|
||||||
|
!probeSuccessNow;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// STEP-UP LOGIC
|
||||||
|
// =====================================================
|
||||||
|
const stepUpByBatteryReserve =
|
||||||
|
batteryPriorityMode &&
|
||||||
|
parsed.batteryCurrentA >= batteryChargeReserveDynamicA;
|
||||||
|
|
||||||
|
const stepUpByForecastMode =
|
||||||
|
!batteryPriorityMode &&
|
||||||
|
(
|
||||||
|
parsed.pvPowerW >= params.pvHoldMinW ||
|
||||||
|
parsed.pvPowerAvg10mW >= params.pvHoldMinW
|
||||||
|
) &&
|
||||||
|
derived.forecastBufferKwh >= -1 &&
|
||||||
|
derived.projectedEndSocAfterCharge >= (derived.endDayGuardSoc - 5) &&
|
||||||
|
cloudToleranceOk;
|
||||||
|
|
||||||
|
const solarShouldStepUp =
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA < derived.physicalCapA &&
|
||||||
|
derived.physicalCapA >= params.currentMinA &&
|
||||||
|
!derived.vebusAcOutSoftReached &&
|
||||||
|
!derived.vebusDcVoltageSoftLow &&
|
||||||
|
!derived.houseLoadHighVsHeadroom &&
|
||||||
|
(
|
||||||
|
stepUpByBatteryReserve ||
|
||||||
|
stepUpByForecastMode ||
|
||||||
|
parsed.pvPowerW >= (context.effectivePvStartMinW + 400)
|
||||||
|
);
|
||||||
|
|
||||||
|
const gridShouldStepUp =
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA < derived.physicalCapA &&
|
||||||
|
derived.physicalCapA >= params.currentMinA &&
|
||||||
|
!derived.vebusAcOutSoftReached &&
|
||||||
|
!derived.vebusDcVoltageSoftLow &&
|
||||||
|
parsed.batteryCurrentA >= gridBatterySoftDynamicA &&
|
||||||
|
!derived.houseLoadHighVsHeadroom;
|
||||||
|
|
||||||
|
const batteryAssistNightShouldStepUp =
|
||||||
|
context.sourceMode === 'battery_assist_night' &&
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA < derived.physicalCapA &&
|
||||||
|
derived.physicalCapA >= params.currentMinA &&
|
||||||
|
!derived.vebusAcOutSoftReached &&
|
||||||
|
!derived.vebusDcVoltageSoftLow &&
|
||||||
|
!derived.houseLoadHighVsHeadroom &&
|
||||||
|
parsed.batteryCurrentA >= batteryChargeReserveDynamicA &&
|
||||||
|
batteryAssistAllowed;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// STEP-DOWN LOGIC
|
||||||
|
// =====================================================
|
||||||
|
const solarShouldStepDownBattery =
|
||||||
|
parsed.batteryCurrentA <= batteryDischargeSoftDynamicA &&
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA > params.currentMinA;
|
||||||
|
|
||||||
|
const solarShouldStepDownCloudDebt =
|
||||||
|
!cloudToleranceOk &&
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA > params.currentMinA;
|
||||||
|
|
||||||
|
const gridShouldStepDownBattery =
|
||||||
|
parsed.batteryCurrentA < gridBatterySoftDynamicA &&
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA > params.currentMinA;
|
||||||
|
|
||||||
|
const batteryAssistNightShouldStepDown =
|
||||||
|
context.sourceMode === 'battery_assist_night' &&
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA > params.currentMinA &&
|
||||||
|
(
|
||||||
|
parsed.batteryCurrentA <= batteryDischargeSoftDynamicA ||
|
||||||
|
parsed.soc <= gridTakeoverTriggerPct ||
|
||||||
|
batteryRecoveryUrgent
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldStepDownVebusPower =
|
||||||
|
derived.vebusAcOutSoftReached &&
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA > params.currentMinA;
|
||||||
|
|
||||||
|
const shouldStepDownVebusVoltage =
|
||||||
|
derived.vebusDcVoltageSoftLow &&
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA > params.currentMinA;
|
||||||
|
|
||||||
|
const shouldStepDownOverload =
|
||||||
|
derived.anyVebusOverload &&
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA > params.currentMinA;
|
||||||
|
|
||||||
|
const shouldStopOverloadAtMin =
|
||||||
|
derived.anyVebusOverload &&
|
||||||
|
derived.currentReferenceA <= params.currentMinA &&
|
||||||
|
sessionObservedActive;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// HARD STOPS / PROTECTIONS
|
||||||
|
// =====================================================
|
||||||
|
const batteryCurrentHardExceeded =
|
||||||
|
parsed.batteryCurrentA <= batteryDischargeHardDynamicA;
|
||||||
|
|
||||||
|
// overload volontairement exclu du hard stop immédiat
|
||||||
|
const mustStopHard =
|
||||||
|
derived.anyVebusLowBattery ||
|
||||||
|
derived.vebusDcVoltageHardLow ||
|
||||||
|
derived.vebusAcOutHardReached ||
|
||||||
|
batteryCurrentHardExceeded;
|
||||||
|
|
||||||
|
const gridBatteryProtectionStop =
|
||||||
|
sessionObservedActive &&
|
||||||
|
isGridSource &&
|
||||||
|
parsed.batteryCurrentA < gridBatteryHardDynamicA;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// OUTPUT
|
||||||
|
// =====================================================
|
||||||
|
return [{
|
||||||
|
json: {
|
||||||
|
...prev,
|
||||||
|
|
||||||
|
batteryPolicy: {
|
||||||
|
socBand,
|
||||||
|
batteryDischargeSoftDynamicA,
|
||||||
|
batteryDischargeHardDynamicA,
|
||||||
|
batteryChargeReserveDynamicA,
|
||||||
|
batterySoftToleranceMinutes,
|
||||||
|
batteryHardToleranceSeconds,
|
||||||
|
batteryRecoveryMinutes,
|
||||||
|
|
||||||
|
gridBatterySoftDynamicA,
|
||||||
|
gridBatteryHardDynamicA,
|
||||||
|
gridForceChargeRelaxSoftA,
|
||||||
|
gridForceChargeRelaxHardA,
|
||||||
|
|
||||||
|
houseSpikeDetected,
|
||||||
|
solarPotentialContext,
|
||||||
|
},
|
||||||
|
|
||||||
|
locks: {
|
||||||
|
nextAllowedWindowStartMs,
|
||||||
|
blockActive,
|
||||||
|
blockStillRelevant,
|
||||||
|
startBlockedByReason,
|
||||||
|
},
|
||||||
|
|
||||||
|
cloud: {
|
||||||
|
sessionObservedActive,
|
||||||
|
batteryNetPowerW,
|
||||||
|
cloudDebtNextKwh,
|
||||||
|
cloudDebtMeaningful,
|
||||||
|
cloudEventDischargingNow,
|
||||||
|
continuingCloudEvent,
|
||||||
|
enteringCloudDebt,
|
||||||
|
cloudDebtActive,
|
||||||
|
cloudDebtStartedMs,
|
||||||
|
cloudDebtDurationMinutes,
|
||||||
|
effectiveCloudToleranceMinutes,
|
||||||
|
cloudToleranceOk,
|
||||||
|
},
|
||||||
|
|
||||||
|
modes: {
|
||||||
|
batteryFullHarvestMode,
|
||||||
|
forecastCommitMode,
|
||||||
|
batteryPriorityMode,
|
||||||
|
},
|
||||||
|
|
||||||
|
nightEvcs: {
|
||||||
|
nightSessionActive,
|
||||||
|
nightSessionNeedsCapture,
|
||||||
|
nightSessionNeedsClear,
|
||||||
|
nightSessionStartSocPct,
|
||||||
|
nightSessionStartAt,
|
||||||
|
nightSessionStartMs,
|
||||||
|
|
||||||
|
batteryHardFloorPct,
|
||||||
|
batteryMorningTargetPct,
|
||||||
|
maxBatteryDischargePoints,
|
||||||
|
gridTakeoverTriggerPct,
|
||||||
|
|
||||||
|
batteryBelowHardFloor,
|
||||||
|
batteryBelowMorningTarget,
|
||||||
|
batteryAtOrBelowGridTakeoverTrigger,
|
||||||
|
|
||||||
|
batteryKwhMissingToTarget,
|
||||||
|
batteryRecoveryMinutesNeeded,
|
||||||
|
batteryRecoveryUrgent,
|
||||||
|
batteryRecoveryAllowedNow,
|
||||||
|
batteryTargetReached,
|
||||||
|
|
||||||
|
batteryAssistAllowed,
|
||||||
|
nightGridTakeoverNeeded,
|
||||||
|
nightPreferredSourceMode,
|
||||||
|
},
|
||||||
|
|
||||||
|
safeguards: {
|
||||||
|
isGridSource,
|
||||||
|
commonSafetyStartOk,
|
||||||
|
commonSafetyHoldOk,
|
||||||
|
profileSocStartFloor,
|
||||||
|
profileSocHoldFloor,
|
||||||
|
profileBatteryDischargeFloorA,
|
||||||
|
batteryCurrentHardExceeded,
|
||||||
|
mustStopHard,
|
||||||
|
gridBatteryProtectionStop,
|
||||||
|
},
|
||||||
|
|
||||||
|
conditions: {
|
||||||
|
solarStartOk,
|
||||||
|
solarHoldOk,
|
||||||
|
gridStartOk,
|
||||||
|
gridHoldOk,
|
||||||
|
|
||||||
|
batteryAssistNightStartOk,
|
||||||
|
batteryAssistNightHoldOk,
|
||||||
|
|
||||||
|
probeStartOk,
|
||||||
|
probeHoldGuardOk,
|
||||||
|
probeWindowActive,
|
||||||
|
probeElapsedReached,
|
||||||
|
probeSuccessNow,
|
||||||
|
probeFailedNow,
|
||||||
|
|
||||||
|
solarShouldStepUp,
|
||||||
|
gridShouldStepUp,
|
||||||
|
batteryAssistNightShouldStepUp,
|
||||||
|
|
||||||
|
solarShouldStepDownBattery,
|
||||||
|
solarShouldStepDownCloudDebt,
|
||||||
|
gridShouldStepDownBattery,
|
||||||
|
batteryAssistNightShouldStepDown,
|
||||||
|
|
||||||
|
shouldStepDownVebusPower,
|
||||||
|
shouldStepDownVebusVoltage,
|
||||||
|
shouldStepDownOverload,
|
||||||
|
shouldStopOverloadAtMin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
|
||||||
951
BATTERY/battery_cloud_locks_new_profil_sans_injection.js
Normal file
951
BATTERY/battery_cloud_locks_new_profil_sans_injection.js
Normal file
@@ -0,0 +1,951 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
const isLocalSolarSource =
|
||||||
|
context.sourceMode === 'solar_only' ||
|
||||||
|
context.sourceMode === 'solar_potential';
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// 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
|
||||||
|
);
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// OUTSIDE-WINDOW LOCAL BYPASS
|
||||||
|
// IMPORTANT:
|
||||||
|
// en auto_solar_plus ou force_charge_now, un contexte local solaire
|
||||||
|
// ne doit plus rester bloqué juste parce qu'on est hors fenêtre.
|
||||||
|
// =====================================================
|
||||||
|
const outsideWindowLocalBypass =
|
||||||
|
context.profile === 'outside_window' &&
|
||||||
|
!strategy.forceGridRequested &&
|
||||||
|
(
|
||||||
|
strategy.forceChargeRequested ||
|
||||||
|
strategy.userStrategy === 'auto_solar_plus'
|
||||||
|
) &&
|
||||||
|
isLocalSolarSource;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// 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' && !outsideWindowLocalBypass)
|
||||||
|
: 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
|
||||||
|
);
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// LEGACY / FORECAST MODES
|
||||||
|
// =====================================================
|
||||||
|
const batteryFullHarvestMode =
|
||||||
|
parsed.forecastModeEnabled &&
|
||||||
|
context.profile !== 'night_hc' &&
|
||||||
|
context.profile !== 'outside_window' &&
|
||||||
|
parsed.soc >= params.batteryFullSoc &&
|
||||||
|
parsed.todayRemainingKwh >= params.batteryFullForecastMinKwh &&
|
||||||
|
derived.projectedEndSocAfterCharge >= (derived.endDayGuardSoc - 2);
|
||||||
|
|
||||||
|
const forecastCommitMode =
|
||||||
|
parsed.forecastModeEnabled &&
|
||||||
|
context.profile !== 'night_hc' &&
|
||||||
|
context.profile !== 'outside_window' &&
|
||||||
|
parsed.soc >= params.forecastCommitSocFloor &&
|
||||||
|
parsed.todayRemainingKwh >= params.forecastCommitMinKwh &&
|
||||||
|
derived.projectedEndSocAfterCharge >= derived.endDayGuardSoc;
|
||||||
|
|
||||||
|
const batteryPriorityMode =
|
||||||
|
!batteryFullHarvestMode && !forecastCommitMode;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// EVCS NIGHT ENGINE
|
||||||
|
// =====================================================
|
||||||
|
const nightSessionActive =
|
||||||
|
context.profile === 'night_hc' &&
|
||||||
|
parsed.evcsVehicleConnected;
|
||||||
|
|
||||||
|
const nightSessionStartSocPct =
|
||||||
|
nightSessionActive
|
||||||
|
? (
|
||||||
|
Number.isFinite(history.nightSessionStartSocPct)
|
||||||
|
? history.nightSessionStartSocPct
|
||||||
|
: parsed.soc
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const nightSessionStartAt =
|
||||||
|
nightSessionActive
|
||||||
|
? (history.nightSessionStartAt || '')
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const nightSessionStartMs =
|
||||||
|
nightSessionActive
|
||||||
|
? (history.nightSessionStartMs ?? null)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const nightSessionNeedsCapture =
|
||||||
|
nightSessionActive &&
|
||||||
|
(
|
||||||
|
!Number.isFinite(history.nightSessionStartSocPct) ||
|
||||||
|
!nightSessionStartAt ||
|
||||||
|
nightSessionStartMs === null
|
||||||
|
);
|
||||||
|
|
||||||
|
const nightSessionNeedsClear =
|
||||||
|
!nightSessionActive &&
|
||||||
|
(
|
||||||
|
Number.isFinite(history.nightSessionStartSocPct) ||
|
||||||
|
!!history.nightSessionStartAt
|
||||||
|
);
|
||||||
|
|
||||||
|
const batteryHardFloorPct =
|
||||||
|
params.evcsNightBatteryHardFloorPct;
|
||||||
|
|
||||||
|
const batteryMorningTargetPct =
|
||||||
|
parsed.todayRemainingKwh >= params.evcsNightStrongForecastKwh
|
||||||
|
? params.evcsNightTargetStrongForecastPct
|
||||||
|
: parsed.todayRemainingKwh >= params.evcsNightMediumForecastKwh
|
||||||
|
? params.evcsNightTargetMediumForecastPct
|
||||||
|
: params.evcsNightTargetWeakForecastPct;
|
||||||
|
|
||||||
|
const maxBatteryDischargePoints =
|
||||||
|
nightSessionActive && Number.isFinite(nightSessionStartSocPct)
|
||||||
|
? Math.max(0, nightSessionStartSocPct - batteryMorningTargetPct)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const gridTakeoverTriggerPct =
|
||||||
|
Math.max(batteryHardFloorPct, batteryMorningTargetPct);
|
||||||
|
|
||||||
|
const batteryKwhMissingToTarget =
|
||||||
|
nightSessionActive
|
||||||
|
? Math.max(
|
||||||
|
0,
|
||||||
|
((batteryMorningTargetPct - parsed.soc) / 100) * cfg.batteryUsableCapacityKwh
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const batteryRecoveryMinutesNeeded =
|
||||||
|
nightSessionActive && params.evcsNightBatteryRecoveryPowerKw > 0
|
||||||
|
? (batteryKwhMissingToTarget / params.evcsNightBatteryRecoveryPowerKw) * 60
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const batteryRecoveryUrgent =
|
||||||
|
nightSessionActive &&
|
||||||
|
Number.isFinite(lifecycle.minutesUntilDeparture) &&
|
||||||
|
lifecycle.minutesUntilDeparture >= 0 &&
|
||||||
|
(
|
||||||
|
batteryRecoveryMinutesNeeded + params.evcsNightDepartureSafetyMin
|
||||||
|
>= lifecycle.minutesUntilDeparture
|
||||||
|
);
|
||||||
|
|
||||||
|
const batteryBelowHardFloor =
|
||||||
|
parsed.soc <= batteryHardFloorPct;
|
||||||
|
|
||||||
|
const batteryBelowMorningTarget =
|
||||||
|
parsed.soc < batteryMorningTargetPct;
|
||||||
|
|
||||||
|
const batteryAtOrBelowGridTakeoverTrigger =
|
||||||
|
parsed.soc <= gridTakeoverTriggerPct;
|
||||||
|
|
||||||
|
const batteryAssistAllowed =
|
||||||
|
nightSessionActive &&
|
||||||
|
!batteryBelowHardFloor &&
|
||||||
|
!batteryRecoveryUrgent &&
|
||||||
|
parsed.soc > gridTakeoverTriggerPct;
|
||||||
|
|
||||||
|
const nightGridTakeoverNeeded =
|
||||||
|
nightSessionActive &&
|
||||||
|
(
|
||||||
|
batteryBelowHardFloor ||
|
||||||
|
batteryAtOrBelowGridTakeoverTrigger ||
|
||||||
|
batteryRecoveryUrgent
|
||||||
|
);
|
||||||
|
|
||||||
|
const nightPreferredSourceMode =
|
||||||
|
!nightSessionActive
|
||||||
|
? 'blocked'
|
||||||
|
: batteryAssistAllowed
|
||||||
|
? 'battery_assist_night'
|
||||||
|
: (
|
||||||
|
batteryBelowHardFloor ||
|
||||||
|
batteryBelowMorningTarget ||
|
||||||
|
batteryRecoveryUrgent
|
||||||
|
)
|
||||||
|
? 'grid_forced'
|
||||||
|
: 'grid_hc_allowed';
|
||||||
|
|
||||||
|
const batteryRecoveryAllowedNow =
|
||||||
|
nightSessionActive &&
|
||||||
|
nightGridTakeoverNeeded &&
|
||||||
|
(
|
||||||
|
batteryBelowMorningTarget ||
|
||||||
|
batteryRecoveryUrgent
|
||||||
|
);
|
||||||
|
|
||||||
|
const batteryTargetReached =
|
||||||
|
nightSessionActive &&
|
||||||
|
parsed.soc >= batteryMorningTargetPct;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// PROFILE-BASED THRESHOLDS
|
||||||
|
// =====================================================
|
||||||
|
const profileSocStartFloor =
|
||||||
|
context.profile === 'morning_solar' ? params.morningSocMin :
|
||||||
|
context.profile === 'midday_hc_or_solar' ? params.middaySocMin :
|
||||||
|
context.profile === 'evening_high_soc' ? params.eveningSocStartMin :
|
||||||
|
params.socStartMin;
|
||||||
|
|
||||||
|
const profileSocHoldFloor =
|
||||||
|
context.profile === 'morning_solar' ? Math.max(params.socStopMin, params.morningSocMin - 10) :
|
||||||
|
context.profile === 'midday_hc_or_solar' ? Math.max(params.socStopMin, params.middaySocMin - 15) :
|
||||||
|
context.profile === 'evening_high_soc' ? params.eveningSocStopMin :
|
||||||
|
params.socStopMin;
|
||||||
|
|
||||||
|
const profileSpecificDischargeFloorA =
|
||||||
|
context.profile === 'morning_solar' ? params.morningBatteryMaxDischargeA :
|
||||||
|
context.profile === 'midday_hc_or_solar' ? params.middayBatteryMaxDischargeA :
|
||||||
|
context.profile === 'evening_high_soc' ? params.eveningBatteryMaxDischargeA :
|
||||||
|
params.batteryDischargeSoftA;
|
||||||
|
|
||||||
|
const profileBatteryDischargeFloorA =
|
||||||
|
Math.max(profileSpecificDischargeFloorA, batteryDischargeSoftDynamicA);
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// GRID RELAX IN force_charge_now
|
||||||
|
// =====================================================
|
||||||
|
// Assouplissement uniquement pour la garde réseau,
|
||||||
|
// sans toucher aux sécurités dures globales.
|
||||||
|
const gridForceChargeRelaxSoftA =
|
||||||
|
strategy.forceChargeRequested && isGridSource
|
||||||
|
? (
|
||||||
|
parsed.soc >= 90 ? 28 :
|
||||||
|
parsed.soc >= 80 ? 16 :
|
||||||
|
parsed.soc >= 60 ? 8 :
|
||||||
|
parsed.soc >= 40 ? 4 :
|
||||||
|
0
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const gridForceChargeRelaxHardA =
|
||||||
|
strategy.forceChargeRequested && isGridSource
|
||||||
|
? (
|
||||||
|
parsed.soc >= 90 ? 39 :
|
||||||
|
parsed.soc >= 80 ? 29 :
|
||||||
|
parsed.soc >= 60 ? 18 :
|
||||||
|
parsed.soc >= 40 ? 8 :
|
||||||
|
0
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const gridBatterySoftDynamicABase =
|
||||||
|
Math.max(params.gridBatterySoftA, batteryDischargeSoftDynamicA);
|
||||||
|
|
||||||
|
const gridBatteryHardDynamicABase =
|
||||||
|
Math.max(params.gridBatteryHardA, batteryDischargeHardDynamicA);
|
||||||
|
|
||||||
|
let gridBatterySoftDynamicA =
|
||||||
|
gridBatterySoftDynamicABase - gridForceChargeRelaxSoftA;
|
||||||
|
|
||||||
|
let gridBatteryHardDynamicA =
|
||||||
|
gridBatteryHardDynamicABase - gridForceChargeRelaxHardA;
|
||||||
|
|
||||||
|
// Garantit que le hard reste plus permissif que le soft d'au moins 3 A
|
||||||
|
if (gridBatteryHardDynamicA > (gridBatterySoftDynamicA - 3)) {
|
||||||
|
gridBatteryHardDynamicA = gridBatterySoftDynamicA - 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// BASE SAFETY
|
||||||
|
// =====================================================
|
||||||
|
const commonSafetyStartOk =
|
||||||
|
derived.canRunAtMinCurrent &&
|
||||||
|
!derived.anyVebusOverload &&
|
||||||
|
!derived.anyVebusLowBattery &&
|
||||||
|
!derived.vebusDcVoltageSoftLow &&
|
||||||
|
!derived.vebusAcOutSoftReached &&
|
||||||
|
parsed.evcsDeviceConnected &&
|
||||||
|
parsed.evcsVehicleConnected &&
|
||||||
|
!parsed.evcsModeIsAuto;
|
||||||
|
|
||||||
|
const commonSafetyHoldOk =
|
||||||
|
derived.canRunAtMinCurrent &&
|
||||||
|
!derived.anyVebusLowBattery &&
|
||||||
|
!derived.vebusDcVoltageHardLow &&
|
||||||
|
parsed.evcsDeviceConnected &&
|
||||||
|
parsed.evcsVehicleConnected &&
|
||||||
|
!parsed.evcsModeIsAuto;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// BATTERY-ASSISTED SOLAR START/HOLD
|
||||||
|
// IMPORTANT:
|
||||||
|
// Installation sans injection:
|
||||||
|
// le PV visible peut rester bas tant que l'EVCS ne démarre pas.
|
||||||
|
// On autorise donc un démarrage/maintien local si la batterie est haute
|
||||||
|
// et que la dette nuage reste faible.
|
||||||
|
// =====================================================
|
||||||
|
const batteryAssistSolarPolicyEligible =
|
||||||
|
!strategy.forceGridRequested &&
|
||||||
|
(
|
||||||
|
strategy.forceChargeRequested ||
|
||||||
|
strategy.userStrategy === 'auto_solar_plus'
|
||||||
|
) &&
|
||||||
|
isLocalSolarSource;
|
||||||
|
|
||||||
|
const batteryAssistSolarStartSocMin = 90;
|
||||||
|
const batteryAssistSolarHoldSocMin = 85;
|
||||||
|
|
||||||
|
const batteryAssistSolarDebtStartMaxKwh = Math.max(
|
||||||
|
cfg.cloudDebtRecoveryThresholdKwh,
|
||||||
|
Math.min(params.cloudDebtMaxKwh, 0.05)
|
||||||
|
);
|
||||||
|
|
||||||
|
const batteryAssistSolarStartOk =
|
||||||
|
batteryAssistSolarPolicyEligible &&
|
||||||
|
commonSafetyStartOk &&
|
||||||
|
!isGridSource &&
|
||||||
|
parsed.soc >= Math.max(profileSocStartFloor, batteryAssistSolarStartSocMin) &&
|
||||||
|
parsed.soc > batteryAssistSolarHoldSocMin &&
|
||||||
|
parsed.batteryCurrentA >= profileBatteryDischargeFloorA &&
|
||||||
|
cloudDebtNextKwh <= batteryAssistSolarDebtStartMaxKwh &&
|
||||||
|
!cloudDebtActive;
|
||||||
|
|
||||||
|
const batteryAssistSolarHoldOk =
|
||||||
|
batteryAssistSolarPolicyEligible &&
|
||||||
|
commonSafetyHoldOk &&
|
||||||
|
!isGridSource &&
|
||||||
|
parsed.soc >= Math.max(profileSocHoldFloor, batteryAssistSolarHoldSocMin) &&
|
||||||
|
parsed.batteryCurrentA >= profileBatteryDischargeFloorA &&
|
||||||
|
cloudToleranceOk;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// START / HOLD CONDITIONS
|
||||||
|
// =====================================================
|
||||||
|
const rawSolarStartOk =
|
||||||
|
(context.sourceMode === 'solar_only' || context.sourceMode === 'solar_potential') &&
|
||||||
|
commonSafetyStartOk &&
|
||||||
|
parsed.soc >= profileSocStartFloor &&
|
||||||
|
parsed.batteryCurrentA >= profileBatteryDischargeFloorA &&
|
||||||
|
(
|
||||||
|
context.sourceMode === 'solar_potential' ||
|
||||||
|
parsed.pvPowerW >= context.effectivePvStartMinW
|
||||||
|
);
|
||||||
|
|
||||||
|
const solarStartOk =
|
||||||
|
rawSolarStartOk || batteryAssistSolarStartOk;
|
||||||
|
|
||||||
|
const rawSolarHoldOk =
|
||||||
|
(context.sourceMode === 'solar_only' || context.sourceMode === 'solar_potential') &&
|
||||||
|
commonSafetyHoldOk &&
|
||||||
|
parsed.soc >= profileSocHoldFloor &&
|
||||||
|
parsed.batteryCurrentA >= profileBatteryDischargeFloorA &&
|
||||||
|
(
|
||||||
|
parsed.pvPowerW >= params.pvHoldMinW ||
|
||||||
|
cloudToleranceOk
|
||||||
|
);
|
||||||
|
|
||||||
|
const solarHoldOk =
|
||||||
|
rawSolarHoldOk || batteryAssistSolarHoldOk;
|
||||||
|
|
||||||
|
const gridStartOk =
|
||||||
|
isGridSource &&
|
||||||
|
commonSafetyStartOk &&
|
||||||
|
parsed.gridAvailable &&
|
||||||
|
parsed.batteryCurrentA >= gridBatterySoftDynamicA;
|
||||||
|
|
||||||
|
const gridHoldOk =
|
||||||
|
isGridSource &&
|
||||||
|
commonSafetyHoldOk &&
|
||||||
|
parsed.gridAvailable &&
|
||||||
|
parsed.batteryCurrentA >= gridBatteryHardDynamicA;
|
||||||
|
|
||||||
|
const batteryAssistNightStartOk =
|
||||||
|
context.sourceMode === 'battery_assist_night' &&
|
||||||
|
commonSafetyStartOk &&
|
||||||
|
nightSessionActive &&
|
||||||
|
batteryAssistAllowed &&
|
||||||
|
parsed.soc > batteryHardFloorPct &&
|
||||||
|
parsed.soc > gridTakeoverTriggerPct &&
|
||||||
|
parsed.batteryCurrentA >= profileBatteryDischargeFloorA;
|
||||||
|
|
||||||
|
const batteryAssistNightHoldOk =
|
||||||
|
context.sourceMode === 'battery_assist_night' &&
|
||||||
|
commonSafetyHoldOk &&
|
||||||
|
nightSessionActive &&
|
||||||
|
batteryAssistAllowed &&
|
||||||
|
parsed.soc > batteryHardFloorPct &&
|
||||||
|
parsed.soc > gridTakeoverTriggerPct &&
|
||||||
|
parsed.batteryCurrentA >= batteryDischargeHardDynamicA;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// PROBE LOGIC — STRICTEMENT LIMITÉE AU MODE PROBE
|
||||||
|
// =====================================================
|
||||||
|
const probeModeActive = context.sourceMode === 'hybrid_probe';
|
||||||
|
|
||||||
|
const probeStartOk =
|
||||||
|
probeModeActive &&
|
||||||
|
commonSafetyStartOk &&
|
||||||
|
(context.morningProbeEligible || context.middayProbeEligible);
|
||||||
|
|
||||||
|
const probeHoldGuardOk =
|
||||||
|
probeModeActive &&
|
||||||
|
commonSafetyHoldOk &&
|
||||||
|
parsed.batteryCurrentA >= profileBatteryDischargeFloorA;
|
||||||
|
|
||||||
|
const probeWindowActive =
|
||||||
|
probeModeActive &&
|
||||||
|
sessionObservedActive &&
|
||||||
|
history.probeStartedMs !== null &&
|
||||||
|
lifecycle.minutesSinceProbeStart !== null &&
|
||||||
|
lifecycle.minutesSinceProbeStart < params.probeMinutes;
|
||||||
|
|
||||||
|
const probeElapsedReached =
|
||||||
|
probeModeActive &&
|
||||||
|
sessionObservedActive &&
|
||||||
|
history.probeStartedMs !== null &&
|
||||||
|
lifecycle.minutesSinceProbeStart !== null &&
|
||||||
|
lifecycle.minutesSinceProbeStart >= params.probeMinutes;
|
||||||
|
|
||||||
|
const probeSuccessNow =
|
||||||
|
probeModeActive &&
|
||||||
|
sessionObservedActive &&
|
||||||
|
history.probeStartedMs !== null &&
|
||||||
|
(
|
||||||
|
parsed.pvPowerW >= params.pvHoldMinW &&
|
||||||
|
parsed.batteryCurrentA >= Math.min(0, batteryDischargeSoftDynamicA)
|
||||||
|
);
|
||||||
|
|
||||||
|
const probeFailedNow =
|
||||||
|
probeModeActive &&
|
||||||
|
probeElapsedReached &&
|
||||||
|
!probeSuccessNow;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// STEP-UP LOGIC
|
||||||
|
// =====================================================
|
||||||
|
const stepUpByBatteryReserve =
|
||||||
|
batteryPriorityMode &&
|
||||||
|
parsed.batteryCurrentA >= batteryChargeReserveDynamicA;
|
||||||
|
|
||||||
|
const stepUpByForecastMode =
|
||||||
|
!batteryPriorityMode &&
|
||||||
|
(
|
||||||
|
parsed.pvPowerW >= params.pvHoldMinW ||
|
||||||
|
parsed.pvPowerAvg10mW >= params.pvHoldMinW
|
||||||
|
) &&
|
||||||
|
derived.forecastBufferKwh >= -1 &&
|
||||||
|
derived.projectedEndSocAfterCharge >= (derived.endDayGuardSoc - 5) &&
|
||||||
|
cloudToleranceOk;
|
||||||
|
|
||||||
|
const solarShouldStepUp =
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA < derived.physicalCapA &&
|
||||||
|
derived.physicalCapA >= params.currentMinA &&
|
||||||
|
!derived.vebusAcOutSoftReached &&
|
||||||
|
!derived.vebusDcVoltageSoftLow &&
|
||||||
|
!derived.houseLoadHighVsHeadroom &&
|
||||||
|
(
|
||||||
|
stepUpByBatteryReserve ||
|
||||||
|
stepUpByForecastMode ||
|
||||||
|
parsed.pvPowerW >= (context.effectivePvStartMinW + 400)
|
||||||
|
);
|
||||||
|
|
||||||
|
const gridShouldStepUp =
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA < derived.physicalCapA &&
|
||||||
|
derived.physicalCapA >= params.currentMinA &&
|
||||||
|
!derived.vebusAcOutSoftReached &&
|
||||||
|
!derived.vebusDcVoltageSoftLow &&
|
||||||
|
parsed.batteryCurrentA >= gridBatterySoftDynamicA &&
|
||||||
|
!derived.houseLoadHighVsHeadroom;
|
||||||
|
|
||||||
|
const batteryAssistNightShouldStepUp =
|
||||||
|
context.sourceMode === 'battery_assist_night' &&
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA < derived.physicalCapA &&
|
||||||
|
derived.physicalCapA >= params.currentMinA &&
|
||||||
|
!derived.vebusAcOutSoftReached &&
|
||||||
|
!derived.vebusDcVoltageSoftLow &&
|
||||||
|
!derived.houseLoadHighVsHeadroom &&
|
||||||
|
parsed.batteryCurrentA >= batteryChargeReserveDynamicA &&
|
||||||
|
batteryAssistAllowed;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// STEP-DOWN LOGIC
|
||||||
|
// =====================================================
|
||||||
|
const solarShouldStepDownBattery =
|
||||||
|
parsed.batteryCurrentA <= batteryDischargeSoftDynamicA &&
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA > params.currentMinA;
|
||||||
|
|
||||||
|
const solarShouldStepDownCloudDebt =
|
||||||
|
!cloudToleranceOk &&
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA > params.currentMinA;
|
||||||
|
|
||||||
|
const gridShouldStepDownBattery =
|
||||||
|
parsed.batteryCurrentA < gridBatterySoftDynamicA &&
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA > params.currentMinA;
|
||||||
|
|
||||||
|
const batteryAssistNightShouldStepDown =
|
||||||
|
context.sourceMode === 'battery_assist_night' &&
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA > params.currentMinA &&
|
||||||
|
(
|
||||||
|
parsed.batteryCurrentA <= batteryDischargeSoftDynamicA ||
|
||||||
|
parsed.soc <= gridTakeoverTriggerPct ||
|
||||||
|
batteryRecoveryUrgent
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldStepDownVebusPower =
|
||||||
|
derived.vebusAcOutSoftReached &&
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA > params.currentMinA;
|
||||||
|
|
||||||
|
const shouldStepDownVebusVoltage =
|
||||||
|
derived.vebusDcVoltageSoftLow &&
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA > params.currentMinA;
|
||||||
|
|
||||||
|
const shouldStepDownOverload =
|
||||||
|
derived.anyVebusOverload &&
|
||||||
|
lifecycle.currentCooldownElapsed &&
|
||||||
|
derived.currentReferenceA > params.currentMinA;
|
||||||
|
|
||||||
|
const shouldStopOverloadAtMin =
|
||||||
|
derived.anyVebusOverload &&
|
||||||
|
derived.currentReferenceA <= params.currentMinA &&
|
||||||
|
sessionObservedActive;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// HARD STOPS / PROTECTIONS
|
||||||
|
// =====================================================
|
||||||
|
const batteryCurrentHardExceeded =
|
||||||
|
parsed.batteryCurrentA <= batteryDischargeHardDynamicA;
|
||||||
|
|
||||||
|
// overload volontairement exclu du hard stop immédiat
|
||||||
|
const mustStopHard =
|
||||||
|
derived.anyVebusLowBattery ||
|
||||||
|
derived.vebusDcVoltageHardLow ||
|
||||||
|
derived.vebusAcOutHardReached ||
|
||||||
|
batteryCurrentHardExceeded;
|
||||||
|
|
||||||
|
const gridBatteryProtectionStop =
|
||||||
|
sessionObservedActive &&
|
||||||
|
isGridSource &&
|
||||||
|
parsed.batteryCurrentA < gridBatteryHardDynamicA;
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// OUTPUT
|
||||||
|
// =====================================================
|
||||||
|
return [{
|
||||||
|
json: {
|
||||||
|
...prev,
|
||||||
|
|
||||||
|
batteryPolicy: {
|
||||||
|
socBand,
|
||||||
|
batteryDischargeSoftDynamicA,
|
||||||
|
batteryDischargeHardDynamicA,
|
||||||
|
batteryChargeReserveDynamicA,
|
||||||
|
batterySoftToleranceMinutes,
|
||||||
|
batteryHardToleranceSeconds,
|
||||||
|
batteryRecoveryMinutes,
|
||||||
|
|
||||||
|
gridBatterySoftDynamicA,
|
||||||
|
gridBatteryHardDynamicA,
|
||||||
|
gridForceChargeRelaxSoftA,
|
||||||
|
gridForceChargeRelaxHardA,
|
||||||
|
|
||||||
|
batteryAssistSolarStartSocMin,
|
||||||
|
batteryAssistSolarHoldSocMin,
|
||||||
|
batteryAssistSolarDebtStartMaxKwh,
|
||||||
|
|
||||||
|
houseSpikeDetected,
|
||||||
|
solarPotentialContext,
|
||||||
|
outsideWindowLocalBypass,
|
||||||
|
},
|
||||||
|
|
||||||
|
locks: {
|
||||||
|
nextAllowedWindowStartMs,
|
||||||
|
blockActive,
|
||||||
|
blockStillRelevant,
|
||||||
|
startBlockedByReason,
|
||||||
|
},
|
||||||
|
|
||||||
|
cloud: {
|
||||||
|
sessionObservedActive,
|
||||||
|
batteryNetPowerW,
|
||||||
|
cloudDebtNextKwh,
|
||||||
|
cloudDebtMeaningful,
|
||||||
|
cloudEventDischargingNow,
|
||||||
|
continuingCloudEvent,
|
||||||
|
enteringCloudDebt,
|
||||||
|
cloudDebtActive,
|
||||||
|
cloudDebtStartedMs,
|
||||||
|
cloudDebtDurationMinutes,
|
||||||
|
effectiveCloudToleranceMinutes,
|
||||||
|
cloudToleranceOk,
|
||||||
|
},
|
||||||
|
|
||||||
|
modes: {
|
||||||
|
batteryFullHarvestMode,
|
||||||
|
forecastCommitMode,
|
||||||
|
batteryPriorityMode,
|
||||||
|
},
|
||||||
|
|
||||||
|
nightEvcs: {
|
||||||
|
nightSessionActive,
|
||||||
|
nightSessionNeedsCapture,
|
||||||
|
nightSessionNeedsClear,
|
||||||
|
nightSessionStartSocPct,
|
||||||
|
nightSessionStartAt,
|
||||||
|
nightSessionStartMs,
|
||||||
|
|
||||||
|
batteryHardFloorPct,
|
||||||
|
batteryMorningTargetPct,
|
||||||
|
maxBatteryDischargePoints,
|
||||||
|
gridTakeoverTriggerPct,
|
||||||
|
|
||||||
|
batteryBelowHardFloor,
|
||||||
|
batteryBelowMorningTarget,
|
||||||
|
batteryAtOrBelowGridTakeoverTrigger,
|
||||||
|
|
||||||
|
batteryKwhMissingToTarget,
|
||||||
|
batteryRecoveryMinutesNeeded,
|
||||||
|
batteryRecoveryUrgent,
|
||||||
|
batteryRecoveryAllowedNow,
|
||||||
|
batteryTargetReached,
|
||||||
|
|
||||||
|
batteryAssistAllowed,
|
||||||
|
nightGridTakeoverNeeded,
|
||||||
|
nightPreferredSourceMode,
|
||||||
|
},
|
||||||
|
|
||||||
|
safeguards: {
|
||||||
|
isGridSource,
|
||||||
|
commonSafetyStartOk,
|
||||||
|
commonSafetyHoldOk,
|
||||||
|
profileSocStartFloor,
|
||||||
|
profileSocHoldFloor,
|
||||||
|
profileBatteryDischargeFloorA,
|
||||||
|
batteryCurrentHardExceeded,
|
||||||
|
mustStopHard,
|
||||||
|
gridBatteryProtectionStop,
|
||||||
|
},
|
||||||
|
|
||||||
|
conditions: {
|
||||||
|
rawSolarStartOk,
|
||||||
|
rawSolarHoldOk,
|
||||||
|
batteryAssistSolarPolicyEligible,
|
||||||
|
batteryAssistSolarStartOk,
|
||||||
|
batteryAssistSolarHoldOk,
|
||||||
|
|
||||||
|
solarStartOk,
|
||||||
|
solarHoldOk,
|
||||||
|
gridStartOk,
|
||||||
|
gridHoldOk,
|
||||||
|
|
||||||
|
batteryAssistNightStartOk,
|
||||||
|
batteryAssistNightHoldOk,
|
||||||
|
|
||||||
|
probeStartOk,
|
||||||
|
probeHoldGuardOk,
|
||||||
|
probeWindowActive,
|
||||||
|
probeElapsedReached,
|
||||||
|
probeSuccessNow,
|
||||||
|
probeFailedNow,
|
||||||
|
|
||||||
|
solarShouldStepUp,
|
||||||
|
gridShouldStepUp,
|
||||||
|
batteryAssistNightShouldStepUp,
|
||||||
|
|
||||||
|
solarShouldStepDownBattery,
|
||||||
|
solarShouldStepDownCloudDebt,
|
||||||
|
gridShouldStepDownBattery,
|
||||||
|
batteryAssistNightShouldStepDown,
|
||||||
|
|
||||||
|
shouldStepDownVebusPower,
|
||||||
|
shouldStepDownVebusVoltage,
|
||||||
|
shouldStepDownOverload,
|
||||||
|
shouldStopOverloadAtMin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
|
||||||
Reference in New Issue
Block a user