// === 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' || profile === 'outside_window') : 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 || afternoonProbeEligible); 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;