// ===================================================== // PROBES / SOLAR POTENTIAL // ===================================================== // IMPORTANT: // On ne met plus effectivePvStartMinW à 0 en force_charge_now, // sinon la nuit 0W >= 0W ferait croire à un faux signal solaire. const effectivePvStartMinW = params.pvStartMinW; const morningProbeEligible = profile === 'morning_solar' && params.probeEnabled && parsed.pvPowerAvg10mW >= params.morningProbePvAvgMinW && parsed.pvTrendW >= params.morningProbePvTrendMinW && parsed.soc >= params.morningSocMin && parsed.batteryCurrentA >= params.morningBatteryMaxDischargeA; const middayProbeEligible = profile === 'midday_hc_or_solar' && params.probeEnabled && parsed.pvPowerAvg10mW >= params.middayProbePvAvgMinW && parsed.soc >= params.middaySocMin && parsed.batteryCurrentA >= params.middayBatteryMaxDischargeA; const cloudDebtOkForPotential = params.cloudDebtPrevKwh <= params.cloudDebtMaxKwh; const batteryOkForPotential = parsed.batteryCurrentA >= -Math.max(1, params.batteryNeutralBandA); const isHighBatteryForPotential = parsed.soc >= 85 && forecastBufferKwh > 0 && cloudDebtOkForPotential && batteryOkForPotential; const potentialAllowed = userStrategy !== 'auto_solar'; // IMPORTANT: // On impose un vrai PV > 0 pour le test sur pvPowerW. // Ça évite qu'en force_charge_now la nuit, 0W soit pris pour un signal solaire. const strongSolarSignal = ( parsed.pvPowerW > 0 && parsed.pvPowerW >= effectivePvStartMinW ) || parsed.pvPowerAvg10mW >= params.pvHoldMinW || (departurePriorityActive && parsed.pvPowerAvg10mW >= params.pvHoldMinW); // ===================================================== // SOURCE MODE // ===================================================== let sourceMode = 'blocked'; if (isStopMode) { sourceMode = 'blocked'; } else if (forceGridRequested) { // Demande explicite de réseau, même si le réseau n'est pas encore présent. // Cela permet aux étages suivants / sorties HA de le réclamer. sourceMode = 'grid_forced'; } else if (forceChargeRequested) { // PATCH EVCS PRIORITAIRE : // force_charge_now + voiture branchée + départ défini => grid_forced if (forceChargeDepartureGridOverride) { sourceMode = 'grid_forced'; } else if (nightSessionActive) { sourceMode = nightPreferredSourceMode; } else if (strongSolarSignal) { sourceMode = 'solar_only'; } else if (isHighBatteryForPotential && potentialAllowed) { sourceMode = 'solar_potential'; } else if (profile === 'midday_hc_or_solar' && parsed.gridAvailable) { sourceMode = params.allowGridHcMidday ? 'grid_hc_allowed' : 'grid_forced'; } else if (parsed.gridAvailable) { sourceMode = 'grid_forced'; } else if (profile === 'midday_hc_or_solar' && middayProbeEligible) { sourceMode = 'hybrid_probe'; } else if (profile === 'morning_solar' && morningProbeEligible) { sourceMode = 'hybrid_probe'; } else { // Si force_charge_now est demandé mais qu'aucune autre source locale ne permet // un démarrage fiable, on garde quand même l'intention EVCS prioritaire. sourceMode = 'grid_forced'; } } else if (profile === 'night_hc') { if (nightSessionActive) { sourceMode = nightPreferredSourceMode; } else { sourceMode = 'blocked'; } } else if (profile === 'midday_hc_or_solar' || profile === 'morning_solar') { if (strongSolarSignal) { sourceMode = 'solar_only'; } else if (isHighBatteryForPotential && potentialAllowed) { sourceMode = 'solar_potential'; } else if (profile === 'midday_hc_or_solar' && middayProbeEligible) { sourceMode = 'hybrid_probe'; } else if (profile === 'morning_solar' && morningProbeEligible) { sourceMode = 'hybrid_probe'; } else if (profile === 'midday_hc_or_solar' && params.allowGridHcMidday && parsed.gridAvailable) { sourceMode = 'grid_hc_allowed'; } else { sourceMode = 'blocked'; } } else if (profile === 'evening_high_soc') { if (strongSolarSignal) { sourceMode = 'solar_only'; } else if (isHighBatteryForPotential && potentialAllowed) { sourceMode = 'solar_potential'; } else { sourceMode = 'blocked'; } } else if (profile === 'outside_window') { if (strongSolarSignal) { sourceMode = 'solar_only'; } else if (isHighBatteryForPotential && potentialAllowed) { sourceMode = 'solar_potential'; } else if (forceChargeRequested) { sourceMode = 'grid_forced'; } else if (parsed.gridAvailable) { sourceMode = 'grid_forced'; } else { sourceMode = 'blocked'; } } const operatingMode = profile; // ===================================================== // OUTPUT // ===================================================== return [{ json: { ...prev, strategy: { userStrategy, isStopMode, isForceMode, forceGridRequested, forceChargeRequested, }, lifecycle: { minutesSinceStart, minutesSinceStop, secondsSinceCurrentChange, minutesSinceProbeStart, minRunElapsed, minStopElapsed, currentCooldownElapsed, minutesUntilDeparture, }, windows: { inNightHcWindow, inMorningWindow, inMiddayWindow, inEveningWindow, }, derived: { acVoltageRef, batteryVoltageRef, houseNonEvcsW, vebusNonEvcsW, totalVebusSoftCeilingW, totalVebusHardCeilingW, safeEvcsByDedicatedCapW, safeEvcsByVebusW, safeEvcsCombinedW, systemCapCurrentA, physicalCapA, canRunAtMinCurrent, endDayGuardSoc, energyNeededForEndDayGuardKwh, forecastBufferKwh, projectedEndSocAfterCharge, currentReferenceA, anyVebusOverload, anyVebusLowBattery, vebusAcOutSoftReached, vebusAcOutHardReached, vebusDcVoltageSoftLow, vebusDcVoltageHardLow, houseLoadHighVsHeadroom, }, context: { departurePriorityActive, forceChargeDepartureGridOverride, morningProbeEligible, middayProbeEligible, isHighBatteryForPotential, strongSolarSignal, effectivePvStartMinW, profile, operatingMode, sourceMode, }, nightEvcsPreview: { nightSessionActive, nightSessionNeedsCapture, nightSessionNeedsClear, nightSessionStartSocPct, nightSessionStartAt, batteryHardFloorPct, batteryMorningTargetPct, maxBatteryDischargePoints, gridTakeoverTriggerPct, batteryBelowHardFloor, batteryBelowMorningTarget, batteryAtOrBelowGridTakeoverTrigger, batteryKwhMissingToTarget, batteryRecoveryMinutesNeeded, batteryRecoveryUrgent, batteryAssistAllowed, nightGridTakeoverNeeded, nightPreferredSourceMode, } } }];