305 lines
9.6 KiB
JavaScript
305 lines
9.6 KiB
JavaScript
function minutesSince(ts, nowMs) {
|
|
if (ts === null || nowMs === null) return null;
|
|
return Math.max(0, (nowMs - ts) / 60000);
|
|
}
|
|
|
|
function secondsSince(ts, nowMs) {
|
|
if (ts === null || nowMs === null) return null;
|
|
return Math.max(0, (nowMs - ts) / 1000);
|
|
}
|
|
|
|
function isWithinWindow(nowMin, startMin, stopMin) {
|
|
if (!Number.isFinite(startMin) || !Number.isFinite(stopMin)) return false;
|
|
if (startMin === stopMin) return false;
|
|
if (startMin < stopMin) return nowMin >= startMin && nowMin < stopMin;
|
|
return nowMin >= startMin || nowMin < stopMin;
|
|
}
|
|
|
|
function clamp(v, min, max) {
|
|
return Math.min(Math.max(v, min), max);
|
|
}
|
|
|
|
const prev = $input.first().json;
|
|
const {
|
|
input,
|
|
nowMs,
|
|
now,
|
|
cfg,
|
|
parsed,
|
|
params,
|
|
history,
|
|
batteryTune
|
|
} = prev;
|
|
|
|
// =====================================================
|
|
// STRATEGY
|
|
// =====================================================
|
|
const userStrategy = (params.userStrategyRaw ?? 'auto_solar').toString().trim().toLowerCase();
|
|
|
|
const isStopMode = userStrategy === 'stop';
|
|
const forceGridRequested = userStrategy === 'force_grid_now';
|
|
const forceChargeRequested = userStrategy === 'force_charge_now';
|
|
const isForceMode = forceGridRequested || forceChargeRequested;
|
|
|
|
// =====================================================
|
|
// LIFECYCLE / TIMING
|
|
// =====================================================
|
|
const minutesSinceStart = minutesSince(history.lastStartMs, nowMs);
|
|
const minutesSinceStop = minutesSince(history.lastStopMs, nowMs);
|
|
const secondsSinceCurrentChange = secondsSince(history.lastCurrentChangeMs, nowMs);
|
|
const minutesSinceProbeStart = minutesSince(history.probeStartedMs, nowMs);
|
|
|
|
const minRunElapsed =
|
|
minutesSinceStart === null ? true : minutesSinceStart >= params.minRunMinutes;
|
|
|
|
const minStopElapsed =
|
|
minutesSinceStop === null ? true : minutesSinceStop >= params.minStopMinutes;
|
|
|
|
const currentCooldownBaseSec = parsed.evcsChargingSwitchOn
|
|
? batteryTune.global.stepDownCooldownSeconds
|
|
: batteryTune.global.stepUpCooldownSeconds;
|
|
|
|
const currentCooldownElapsed =
|
|
secondsSinceCurrentChange === null
|
|
? true
|
|
: secondsSinceCurrentChange >= Math.max(
|
|
params.currentChangeCooldownSeconds,
|
|
currentCooldownBaseSec
|
|
);
|
|
|
|
// =====================================================
|
|
// DEPARTURE PRIORITY
|
|
// =====================================================
|
|
const minutesUntilDeparture =
|
|
params.departureTimeMin === null
|
|
? null
|
|
: (() => {
|
|
const rawDiff = params.departureTimeMin - now.minNow;
|
|
return rawDiff >= 0 ? rawDiff : (1440 - now.minNow + params.departureTimeMin);
|
|
})();
|
|
|
|
const departurePriorityActive =
|
|
parsed.departurePriorityEnabled &&
|
|
Number.isFinite(minutesUntilDeparture) &&
|
|
minutesUntilDeparture >= 0 &&
|
|
minutesUntilDeparture <= cfg.departurePriorityWindowMinutes &&
|
|
parsed.evcsVehicleConnected;
|
|
|
|
// =====================================================
|
|
// EVCS PRIORITY OVERRIDE FOR FORCE CHARGE
|
|
// force_charge_now + voiture branchée + départ défini = grid_forced
|
|
// =====================================================
|
|
const forceChargeDepartureGridOverride =
|
|
forceChargeRequested &&
|
|
parsed.evcsVehicleConnected &&
|
|
Number.isFinite(minutesUntilDeparture) &&
|
|
minutesUntilDeparture >= 0;
|
|
|
|
// =====================================================
|
|
// SYSTEM DERIVED VALUES
|
|
// =====================================================
|
|
const acVoltageRef =
|
|
Number.isFinite(parsed.vebusAcOutVoltageV) && parsed.vebusAcOutVoltageV > 180
|
|
? parsed.vebusAcOutVoltageV
|
|
: cfg.nominalAcVoltageV;
|
|
|
|
const batteryVoltageRef =
|
|
Number.isFinite(parsed.vebusDcVoltageV) && parsed.vebusDcVoltageV > 0
|
|
? parsed.vebusDcVoltageV
|
|
: 51.2;
|
|
|
|
const houseNonEvcsW = Math.max(0, parsed.houseAcPowerW - parsed.evcsAcPowerW);
|
|
const vebusNonEvcsW = Math.max(0, parsed.vebusAcOutPowerW - parsed.evcsAcPowerW);
|
|
|
|
const totalVebusSoftCeilingW = Math.max(6500, params.acLoadsMaxSafeW || 8500);
|
|
const totalVebusHardCeilingW = totalVebusSoftCeilingW + 400;
|
|
|
|
const safeEvcsByDedicatedCapW = Math.max(0, cfg.evcsDedicatedMaxW - cfg.systemReserveW);
|
|
const safeEvcsByVebusW = Math.max(0, totalVebusSoftCeilingW - vebusNonEvcsW - cfg.systemReserveW);
|
|
const safeEvcsCombinedW = Math.max(0, Math.min(safeEvcsByDedicatedCapW, safeEvcsByVebusW));
|
|
|
|
const systemCapCurrentA = Math.max(0, Math.floor(safeEvcsCombinedW / acVoltageRef));
|
|
const physicalCapA = clamp(systemCapCurrentA, 0, params.currentMaxAutoA);
|
|
const canRunAtMinCurrent = physicalCapA >= params.currentMinA;
|
|
|
|
const endDayGuardSoc = Math.max(params.endOfDaySocGuard, params.endOfDaySocTarget);
|
|
|
|
const energyNeededForEndDayGuardKwh = Math.max(
|
|
0,
|
|
((endDayGuardSoc - parsed.soc) / 100) * cfg.batteryUsableCapacityKwh
|
|
);
|
|
|
|
const forecastBufferKwh =
|
|
parsed.todayRemainingKwh -
|
|
params.baseLoadRemainingKwh -
|
|
params.flexibleSafetyMarginKwh -
|
|
params.requiredEnergyKwh -
|
|
energyNeededForEndDayGuardKwh;
|
|
|
|
const projectedEndSocAfterCharge = clamp(
|
|
parsed.soc + (
|
|
(
|
|
parsed.todayRemainingKwh -
|
|
params.baseLoadRemainingKwh -
|
|
params.flexibleSafetyMarginKwh -
|
|
params.requiredEnergyKwh
|
|
) / cfg.batteryUsableCapacityKwh
|
|
) * 100,
|
|
0,
|
|
100
|
|
);
|
|
|
|
const currentReferenceA = clamp(
|
|
Number.isFinite(parsed.evcsCommandCurrentA)
|
|
? parsed.evcsCommandCurrentA
|
|
: Number.isFinite(parsed.evcsSetCurrentA)
|
|
? parsed.evcsSetCurrentA
|
|
: params.targetCurrentAutoA,
|
|
0,
|
|
32
|
|
);
|
|
|
|
const anyVebusOverload = parsed.vebusOverload || parsed.vebusL1Overload;
|
|
const anyVebusLowBattery = parsed.vebusLowBattery || parsed.vebusL1LowBattery;
|
|
|
|
const vebusAcOutSoftReached = parsed.vebusAcOutPowerW >= totalVebusSoftCeilingW;
|
|
const vebusAcOutHardReached = parsed.vebusAcOutPowerW >= totalVebusHardCeilingW;
|
|
|
|
const vebusDcVoltageSoftLow =
|
|
Number.isFinite(parsed.vebusDcVoltageV) &&
|
|
parsed.vebusDcVoltageV > 0 &&
|
|
parsed.vebusDcVoltageV <= cfg.vebusDcVoltageSoftFloorV;
|
|
|
|
const vebusDcVoltageHardLow =
|
|
Number.isFinite(parsed.vebusDcVoltageV) &&
|
|
parsed.vebusDcVoltageV > 0 &&
|
|
parsed.vebusDcVoltageV <= cfg.vebusDcVoltageHardFloorV;
|
|
|
|
const houseLoadHighVsHeadroom = houseNonEvcsW > params.houseAcHeadroomW;
|
|
|
|
// =====================================================
|
|
// WINDOWS / PROFILE
|
|
// =====================================================
|
|
const inNightHcWindow = isWithinWindow(now.minNow, params.hcNightStartMin, params.hcNightStopMin);
|
|
const inMorningWindow = isWithinWindow(now.minNow, params.morningStartMin, params.morningStopMin);
|
|
const inMiddayWindow = isWithinWindow(now.minNow, params.hcMiddayStartMin, params.hcMiddayStopMin);
|
|
const inEveningWindow = isWithinWindow(now.minNow, params.eveningStartMin, params.eveningStopMin);
|
|
|
|
const profile =
|
|
inNightHcWindow ? 'night_hc' :
|
|
inMorningWindow ? 'morning_solar' :
|
|
inMiddayWindow ? 'midday_hc_or_solar' :
|
|
inEveningWindow ? 'evening_high_soc' :
|
|
'outside_window';
|
|
|
|
// =====================================================
|
|
// EVCS NIGHT PREVIEW ENGINE
|
|
// =====================================================
|
|
const nightSessionActive =
|
|
profile === 'night_hc' &&
|
|
parsed.evcsVehicleConnected;
|
|
|
|
const nightSessionStartSocPct =
|
|
nightSessionActive
|
|
? (
|
|
Number.isFinite(history.nightSessionStartSocPct)
|
|
? history.nightSessionStartSocPct
|
|
: parsed.soc
|
|
)
|
|
: null;
|
|
|
|
const nightSessionStartAt =
|
|
nightSessionActive
|
|
? (history.nightSessionStartAt || '')
|
|
: '';
|
|
|
|
const nightSessionNeedsCapture =
|
|
nightSessionActive &&
|
|
(
|
|
!Number.isFinite(history.nightSessionStartSocPct) ||
|
|
!history.nightSessionStartAt
|
|
);
|
|
|
|
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 batteryBelowHardFloor =
|
|
parsed.soc <= batteryHardFloorPct;
|
|
|
|
const batteryBelowMorningTarget =
|
|
parsed.soc < batteryMorningTargetPct;
|
|
|
|
const batteryAtOrBelowGridTakeoverTrigger =
|
|
parsed.soc <= gridTakeoverTriggerPct;
|
|
|
|
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(minutesUntilDeparture) &&
|
|
minutesUntilDeparture >= 0 &&
|
|
(
|
|
batteryRecoveryMinutesNeeded + params.evcsNightDepartureSafetyMin
|
|
>= minutesUntilDeparture
|
|
);
|
|
|
|
const batteryAssistAllowed =
|
|
nightSessionActive &&
|
|
parsed.soc > batteryHardFloorPct &&
|
|
parsed.soc > gridTakeoverTriggerPct &&
|
|
!batteryRecoveryUrgent;
|
|
|
|
const nightGridTakeoverNeeded =
|
|
nightSessionActive &&
|
|
(
|
|
batteryBelowHardFloor ||
|
|
batteryAtOrBelowGridTakeoverTrigger ||
|
|
batteryRecoveryUrgent
|
|
);
|
|
|
|
const nightPreferredSourceMode =
|
|
!nightSessionActive
|
|
? 'blocked'
|
|
: batteryAssistAllowed
|
|
? 'battery_assist_night'
|
|
: (
|
|
batteryBelowHardFloor ||
|
|
batteryBelowMorningTarget ||
|
|
batteryRecoveryUrgent
|
|
)
|
|
? 'grid_forced'
|
|
: 'grid_hc_allowed';
|