218 lines
7.7 KiB
JavaScript
218 lines
7.7 KiB
JavaScript
// === REASON-BASED LOCKS ===
|
|
const nextWindowCandidates = [
|
|
nextDailyOccurrenceMs(morningStartMin, nowMs),
|
|
nextDailyOccurrenceMs(hcMiddayStartMin, nowMs),
|
|
nextDailyOccurrenceMs(eveningStartMin, nowMs),
|
|
nextDailyOccurrenceMs(hcNightStartMin, nowMs),
|
|
].filter(v => Number.isFinite(v));
|
|
const nextAllowedWindowStartMs =
|
|
nextWindowCandidates.length > 0 ? Math.min(...nextWindowCandidates) : null;
|
|
const blockActive = blockedUntilMs !== null && blockedUntilMs > nowMs + 1000;
|
|
const blockStillRelevant =
|
|
lastBlockReason === 'outside_window' ? profile === 'outside_window' :
|
|
lastBlockReason === 'probe_failed' ? (profile === 'morning_solar' || profile === 'midday_hc_or_solar') :
|
|
lastBlockReason === 'battery_protection_stop' ? (
|
|
batteryCurrentA < -batteryNeutralBandA || anyVebusLowBattery || vebusDcVoltageSoftLow
|
|
) :
|
|
lastBlockReason === 'grid_battery_guard' ? sourceMode === 'grid_hc_allowed' :
|
|
false;
|
|
const startBlockedByReason = blockActive && blockStillRelevant;
|
|
|
|
// === CLOUD DEBT ===
|
|
const sessionObservedActive = evcsChargingSwitchOn || controlLatch || chargePermissionNow;
|
|
const batteryNetPowerW = batteryCurrentA * batteryVoltageRef;
|
|
let cloudDebtNextKwh = cloudDebtPrevKwh;
|
|
if (sessionObservedActive) {
|
|
if (batteryNetPowerW < 0 && sourceMode !== 'grid_hc_allowed') {
|
|
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, cloudDebtMaxKwh * 4);
|
|
const cloudDebtMeaningful = cloudDebtNextKwh > CFG.cloudDebtMeaningfulThresholdKwh;
|
|
const cloudEventDischargingNow =
|
|
sessionObservedActive &&
|
|
sourceMode !== 'grid_hc_allowed' &&
|
|
batteryCurrentA < -Math.max(1, batteryNeutralBandA) &&
|
|
cloudDebtMeaningful;
|
|
const continuingCloudEvent =
|
|
cloudEventDischargingNow &&
|
|
cloudDebtStartedMsPrev !== null &&
|
|
cloudDebtPrevKwh > CFG.cloudDebtRecoveryThresholdKwh;
|
|
const enteringCloudDebt = cloudEventDischargingNow && !continuingCloudEvent;
|
|
const cloudDebtActive = cloudEventDischargingNow;
|
|
const cloudDebtStartedMs =
|
|
cloudEventDischargingNow
|
|
? (continuingCloudEvent ? cloudDebtStartedMsPrev : nowMs)
|
|
: null;
|
|
const cloudDebtDurationMinutes =
|
|
cloudDebtActive && cloudDebtStartedMs !== null
|
|
? Math.max(0, (nowMs - cloudDebtStartedMs) / 60000)
|
|
: 0;
|
|
const cloudToleranceOk =
|
|
cloudDebtNextKwh <= cloudDebtMaxKwh &&
|
|
(
|
|
!cloudDebtActive ||
|
|
cloudDebtDurationMinutes <= cloudToleranceMinutes
|
|
);
|
|
|
|
// === LEGACY SOLAR MODES KEPT ===
|
|
const batteryFullHarvestMode =
|
|
forecastModeEnabled &&
|
|
profile !== 'night_hc' &&
|
|
profile !== 'outside_window' &&
|
|
soc >= batteryFullSoc &&
|
|
todayRemainingKwh >= batteryFullForecastMinKwh &&
|
|
projectedEndSocAfterCharge >= (endDayGuardSoc - 2);
|
|
const forecastCommitMode =
|
|
forecastModeEnabled &&
|
|
profile !== 'night_hc' &&
|
|
profile !== 'outside_window' &&
|
|
soc >= forecastCommitSocFloor &&
|
|
todayRemainingKwh >= forecastCommitMinKwh &&
|
|
projectedEndSocAfterCharge >= endDayGuardSoc;
|
|
const batteryPriorityMode = !batteryFullHarvestMode && !forecastCommitMode;
|
|
|
|
// === BASE CONDITIONS ===
|
|
const commonSafetyStartOk =
|
|
canRunAtMinCurrent &&
|
|
!anyVebusOverload &&
|
|
!anyVebusLowBattery &&
|
|
!vebusDcVoltageSoftLow &&
|
|
!vebusAcOutSoftReached &&
|
|
evcsDeviceConnected &&
|
|
evcsVehicleConnected &&
|
|
!evcsModeIsAuto;
|
|
const commonSafetyHoldOk =
|
|
canRunAtMinCurrent &&
|
|
!anyVebusOverload &&
|
|
!anyVebusLowBattery &&
|
|
!vebusDcVoltageHardLow &&
|
|
evcsDeviceConnected &&
|
|
evcsVehicleConnected &&
|
|
!evcsModeIsAuto;
|
|
const profileSocStartFloor =
|
|
profile === 'morning_solar' ? morningSocMin :
|
|
profile === 'midday_hc_or_solar' ? middaySocMin :
|
|
profile === 'evening_high_soc' ? eveningSocStartMin :
|
|
socStartMin;
|
|
const profileSocHoldFloor =
|
|
profile === 'morning_solar' ? Math.max(socStopMin, morningSocMin - 10) :
|
|
profile === 'midday_hc_or_solar' ? Math.max(socStopMin, middaySocMin - 15) :
|
|
profile === 'evening_high_soc' ? eveningSocStopMin :
|
|
socStopMin;
|
|
const profileBatteryDischargeFloorA =
|
|
profile === 'morning_solar' ? morningBatteryMaxDischargeA :
|
|
profile === 'midday_hc_or_solar' ? middayBatteryMaxDischargeA :
|
|
profile === 'evening_high_soc' ? eveningBatteryMaxDischargeA :
|
|
batteryDischargeSoftA;
|
|
const solarStartOk =
|
|
sourceMode === 'solar_only' || sourceMode === 'solar_potential' &&
|
|
commonSafetyStartOk &&
|
|
soc >= profileSocStartFloor &&
|
|
batteryCurrentA >= profileBatteryDischargeFloorA &&
|
|
(sourceMode === 'solar_potential' || pvPowerW >= effectivePvStartMinW);
|
|
const solarHoldOk =
|
|
commonSafetyHoldOk &&
|
|
soc >= profileSocHoldFloor &&
|
|
batteryCurrentA >= profileBatteryDischargeFloorA &&
|
|
(pvPowerW >= pvHoldMinW || cloudToleranceOk);
|
|
const gridStartOk =
|
|
(sourceMode === 'grid_hc_allowed' || sourceMode === 'grid_forced') &&
|
|
commonSafetyStartOk &&
|
|
gridIsOn &&
|
|
batteryCurrentA >= gridBatterySoftA;
|
|
const gridHoldOk =
|
|
commonSafetyHoldOk &&
|
|
(sourceMode === 'grid_hc_allowed' || sourceMode === 'grid_forced') &&
|
|
gridIsOn &&
|
|
batteryCurrentA >= gridBatteryHardA;
|
|
const probeStartOk =
|
|
sourceMode === 'hybrid_probe' &&
|
|
commonSafetyStartOk &&
|
|
(morningProbeEligible || middayProbeEligible);
|
|
const probeHoldGuardOk =
|
|
commonSafetyHoldOk &&
|
|
batteryCurrentA >= profileBatteryDischargeFloorA;
|
|
const probeWindowActive =
|
|
sessionObservedActive &&
|
|
probeStartedMs !== null &&
|
|
minutesSinceProbeStart !== null &&
|
|
minutesSinceProbeStart < probeMinutes;
|
|
const probeElapsedReached =
|
|
sessionObservedActive &&
|
|
probeStartedMs !== null &&
|
|
minutesSinceProbeStart !== null &&
|
|
minutesSinceProbeStart >= probeMinutes;
|
|
const probeSuccessNow =
|
|
sessionObservedActive &&
|
|
probeStartedMs !== null &&
|
|
(
|
|
pvPowerW >= pvHoldMinW &&
|
|
batteryCurrentA >= Math.min(0, batteryDischargeSoftA)
|
|
);
|
|
const probeFailedNow = probeElapsedReached && !probeSuccessNow;
|
|
|
|
// === STEP LOGIC ===
|
|
const stepUpByBatteryReserve =
|
|
batteryPriorityMode &&
|
|
batteryCurrentA >= batteryReserveUpA;
|
|
const stepUpByForecastMode =
|
|
!batteryPriorityMode &&
|
|
(pvPowerW >= pvHoldMinW || pvPowerAvg10mW >= pvHoldMinW) &&
|
|
forecastBufferKwh >= -1 &&
|
|
projectedEndSocAfterCharge >= (endDayGuardSoc - 5) &&
|
|
cloudToleranceOk;
|
|
const solarShouldStepUp =
|
|
currentCooldownElapsed &&
|
|
currentReferenceA < physicalCapA &&
|
|
physicalCapA >= currentMinA &&
|
|
!vebusAcOutSoftReached &&
|
|
!vebusDcVoltageSoftLow &&
|
|
!houseLoadHighVsHeadroom &&
|
|
(stepUpByBatteryReserve || stepUpByForecastMode || pvPowerW >= (effectivePvStartMinW + 400));
|
|
const gridShouldStepUp =
|
|
currentCooldownElapsed &&
|
|
currentReferenceA < physicalCapA &&
|
|
physicalCapA >= currentMinA &&
|
|
!vebusAcOutSoftReached &&
|
|
!vebusDcVoltageSoftLow &&
|
|
batteryCurrentA >= gridBatterySoftA &&
|
|
!houseLoadHighVsHeadroom;
|
|
const solarShouldStepDownBattery =
|
|
batteryCurrentA <= batteryDischargeSoftA &&
|
|
currentCooldownElapsed &&
|
|
currentReferenceA > currentMinA;
|
|
const solarShouldStepDownCloudDebt =
|
|
!cloudToleranceOk &&
|
|
currentCooldownElapsed &&
|
|
currentReferenceA > currentMinA;
|
|
const gridShouldStepDownBattery =
|
|
batteryCurrentA < gridBatterySoftA &&
|
|
currentCooldownElapsed &&
|
|
currentReferenceA > currentMinA;
|
|
const shouldStepDownVebusPower =
|
|
vebusAcOutSoftReached &&
|
|
currentCooldownElapsed &&
|
|
currentReferenceA > currentMinA;
|
|
const shouldStepDownVebusVoltage =
|
|
vebusDcVoltageSoftLow &&
|
|
currentCooldownElapsed &&
|
|
currentReferenceA > currentMinA;
|
|
const batteryCurrentHardExceeded = batteryCurrentA <= batteryDischargeHardA;
|
|
|
|
// === HARD STOPS ===
|
|
const mustStopHard =
|
|
anyVebusOverload ||
|
|
anyVebusLowBattery ||
|
|
vebusDcVoltageHardLow ||
|
|
vebusAcOutHardReached ||
|
|
batteryCurrentHardExceeded;
|
|
const gridBatteryProtectionStop =
|
|
sessionObservedActive &&
|
|
sourceMode === 'grid_hc_allowed' &&
|
|
batteryCurrentA < gridBatteryHardA;
|