111 lines
3.7 KiB
JavaScript
111 lines
3.7 KiB
JavaScript
// =====================================================
|
|
// 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
|
|
);
|