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, } } }];