// ===================================================== // 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; // ===================================================== // 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; // ===================================================== // 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;