// === STRATÉGIE UTILISATEUR (V2.0 - NEW) === const userStrategyRaw = input.victron_evcs_strategy || 'auto_solar'; const userStrategy = low(userStrategyRaw); // auto_solar | auto_solar_plus | force_grid_now | force_charge_now | stop // Mode Force (Ignore HC strict & PV minimaux) const isForceMode = (userStrategy === 'force_grid_now' || userStrategy === 'force_charge_now'); const isStopMode = (userStrategy === 'stop'); // Override for Force modes let effectivePvStartMinW = pvStartMinW; let effectiveAllowGridMidday = low(input.allow_grid_hc_midday || 'off') === 'on'; if (isForceMode) { effectivePvStartMinW = 0; effectiveAllowGridMidday = true; } // === OPTIONAL V1.4 INPUTS === const hcNightStartMin = parseTimeToMinutes(input.hc_night_start_time, 23 * 60); const hcNightStopMin = parseTimeToMinutes(input.hc_night_stop_time, 6 * 60); const morningStartMin = parseTimeToMinutes(input.morning_start_time, dayStartTimeMin); const morningStopMin = parseTimeToMinutes(input.morning_stop_time, 11 * 60); const hcMiddayStartMin = parseTimeToMinutes(input.hc_midday_start_time, 12 * 60 + 24); const hcMiddayStopMin = parseTimeToMinutes(input.hc_midday_stop_time, 14 * 60 + 24); const eveningStartMin = parseTimeToMinutes(input.evening_start_time, 17 * 60); const eveningStopMin = parseTimeToMinutes(input.evening_stop_time, dayStopTimeMin); const allowGridHcNight = low(input.allow_grid_hc_night || 'off') === 'on'; const allowGridHcMidday = effectiveAllowGridMidday; // Utilise le override si force mode const probeEnabled = low(input.probe_enabled || 'off') === 'on'; const probeMinutes = Math.max(1, num(input.probe_minutes, 3)); const probeRetryMinutes = Math.max(5, num(input.probe_retry_minutes, 45)); const probeCurrentA = clamp(num(input.probe_current_a, currentMinA), currentMinA, currentMaxAutoA); const gridBatterySoftA = num(input.grid_battery_soft_a, -3); const gridBatteryHardA = num(input.grid_battery_hard_a, -6); const morningProbePvAvgMinW = Math.max(0, num(input.morning_probe_pv_avg_min_w, 1500)); const morningProbePvTrendMinW = num(input.morning_probe_pv_trend_min_w, 0); const middayProbePvAvgMinW = Math.max(0, num(input.midday_probe_pv_avg_min_w, 1200)); const morningSocMin = clamp(num(input.morning_soc_min, 35), 0, 100); const middaySocMin = clamp(num(input.midday_soc_min, 75), 0, 100); const eveningSocStartMin = clamp(num(input.evening_soc_start_min, 85), 0, 100); const eveningSocStopMin = clamp(num(input.evening_soc_stop_min, 80), 0, 100); const morningBatteryMaxDischargeA = num(input.morning_battery_max_discharge_a, -20); const middayBatteryMaxDischargeA = num(input.midday_battery_max_discharge_a, -40); const eveningBatteryMaxDischargeA = num(input.evening_battery_max_discharge_a, -30); const lastBlockReason = (input.last_block_reason ?? '').toString().trim() || 'none'; const blockedUntilAt = (input.blocked_until ?? '').toString().trim(); const probeStartedAt = (input.probe_started_at ?? '').toString().trim(); const lastStartAt = (input.last_start_at ?? '').toString().trim(); const lastStopAt = (input.last_stop_at ?? '').toString().trim(); const lastCurrentChangeAt = (input.last_current_change_at ?? '').toString().trim(); const lastSignature = (input.last_signature ?? '').toString().trim(); const cloudDebtStartedAt = (input.cloudDebtStartedAt ?? '').toString().trim(); const lastStartMs = parseParisStampToMs(lastStartAt); const lastStopMs = parseParisStampToMs(lastStopAt); const lastCurrentChangeMs = parseParisStampToMs(lastCurrentChangeAt); const blockedUntilMs = parseParisStampToMs(blockedUntilAt); const probeStartedMs = parseParisStampToMs(probeStartedAt); const cloudDebtStartedMsPrev = parseParisStampToMs(cloudDebtStartedAt); const minutesSinceStart = minutesSince(lastStartMs, nowMs); const minutesSinceStop = minutesSince(lastStopMs, nowMs); const secondsSinceCurrentChange = secondsSince(lastCurrentChangeMs, nowMs); const minutesSinceProbeStart = minutesSince(probeStartedMs, nowMs); const minRunElapsed = minutesSinceStart === null ? true : minutesSinceStart >= minRunMinutes; const minStopElapsed = minutesSinceStop === null ? true : minutesSinceStop >= minStopMinutes; const currentCooldownElapsed = secondsSinceCurrentChange === null ? true : secondsSinceCurrentChange >= currentChangeCooldownSeconds; const minutesUntilDeparture = departureTimeMin === null ? null : (() => { const rawDiff = departureTimeMin - now.minNow; return rawDiff >= 0 ? rawDiff : (1440 - now.minNow + departureTimeMin); })(); const departurePriorityActive = departurePriorityEnabled && Number.isFinite(minutesUntilDeparture) && minutesUntilDeparture >= 0 && minutesUntilDeparture <= CFG.departurePriorityWindowMinutes && evcsVehicleConnected; // === VE.BUS INPUTS === const vebusOverload = low(input.vebus_overload || 'off') === 'on'; const vebusL1Overload = low(input.vebus_l1_overload || 'off') === 'on'; const vebusLowBattery = low(input.vebus_low_battery || 'off') === 'on'; const vebusL1LowBattery = low(input.vebus_l1_low_battery || 'off') === 'on'; const vebusAcOutPowerW = Math.max(0, num(input.vebus_ac_out_l1_power_w, 0)); const vebusAcOutCurrentA = Math.max(0, num(input.vebus_ac_out_l1_current_a, 0)); const vebusAcOutVoltageV = num(input.vebus_ac_out_l1_voltage_v, CFG.nominalAcVoltageV); const vebusDcVoltageV = num(input.vebus_dc_voltage_v, NaN); const vebusDcCurrentA = num(input.vebus_dc_current_a, NaN); const vebusDcPowerW = num(input.vebus_dc_power_w, NaN); const vebusState = num(input.vebus_state, NaN); const vebusMainState = num(input.vebus_main_state, NaN); // === DERIVED === const acVoltageRef = Number.isFinite(vebusAcOutVoltageV) && vebusAcOutVoltageV > 180 ? vebusAcOutVoltageV : CFG.nominalAcVoltageV; const batteryVoltageRef = Number.isFinite(vebusDcVoltageV) && vebusDcVoltageV > 0 ? vebusDcVoltageV : 51.2; const houseNonEvcsW = Math.max(0, houseAcPowerW - evcsAcPowerW); const vebusNonEvcsW = Math.max(0, vebusAcOutPowerW - evcsAcPowerW); const totalVebusSoftCeilingW = Math.max(6500, acLoadsMaxSafeW || 8500); const totalVebusHardCeilingW = totalVebusSoftCeilingW + 400; const safeEvcsByDedicatedCapW = Math.max(0, CFG.evcsDedicatedMaxW - CFG.systemReserveW); const safeEvcsByVebusW = Math.max(0, totalVebusSoftCeilingW - vebusNonEvcsW - CFG.systemReserveW); const safeEvcsCombinedW = Math.max(0, Math.min(safeEvcsByDedicatedCapW, safeEvcsByVebusW)); const systemCapCurrentA = Math.max(0, Math.floor(safeEvcsCombinedW / acVoltageRef)); const physicalCapA = clamp(systemCapCurrentA, 0, currentMaxAutoA); const canRunAtMinCurrent = physicalCapA >= currentMinA; const endDayGuardSoc = Math.max(endOfDaySocGuard, endOfDaySocTarget); const energyNeededForEndDayGuardKwh = Math.max(0, ((endDayGuardSoc - soc) / 100) * CFG.batteryUsableCapacityKwh); const forecastBufferKwh = todayRemainingKwh - baseLoadRemainingKwh - flexibleSafetyMarginKwh - requiredEnergyKwh - energyNeededForEndDayGuardKwh; const projectedEndSocAfterCharge = clamp( soc + ( ( todayRemainingKwh - baseLoadRemainingKwh - flexibleSafetyMarginKwh - requiredEnergyKwh ) / CFG.batteryUsableCapacityKwh ) * 100, 0, 100 ); const currentReferenceA = clamp( Number.isFinite(evcsCommandCurrentA) ? evcsCommandCurrentA : Number.isFinite(evcsSetCurrentA) ? evcsSetCurrentA : targetCurrentAutoA, 0, 32 ); const anyVebusOverload = vebusOverload || vebusL1Overload; const anyVebusLowBattery = vebusLowBattery || vebusL1LowBattery; const vebusAcOutSoftReached = vebusAcOutPowerW >= totalVebusSoftCeilingW; const vebusAcOutHardReached = vebusAcOutPowerW >= totalVebusHardCeilingW; const vebusDcVoltageSoftLow = Number.isFinite(vebusDcVoltageV) && vebusDcVoltageV > 0 && vebusDcVoltageV <= CFG.vebusDcVoltageSoftFloorV; const vebusDcVoltageHardLow = Number.isFinite(vebusDcVoltageV) && vebusDcVoltageV > 0 && vebusDcVoltageV <= CFG.vebusDcVoltageHardFloorV; const houseLoadHighVsHeadroom = houseNonEvcsW > houseAcHeadroomW; // === WINDOWS / PROFILE / SOURCE MODE (With Solar Potential Logic) === const inNightHcWindow = isWithinWindow(now.minNow, hcNightStartMin, hcNightStopMin); const inMorningWindow = isWithinWindow(now.minNow, morningStartMin, morningStopMin); const inMiddayWindow = isWithinWindow(now.minNow, hcMiddayStartMin, hcMiddayStopMin); const inEveningWindow = isWithinWindow(now.minNow, eveningStartMin, eveningStopMin); const profile = inNightHcWindow ? 'night_hc' : inMorningWindow ? 'morning_solar' : inMiddayWindow ? 'midday_hc_or_solar' : inEveningWindow ? 'evening_high_soc' : 'outside_window'; const morningProbeEligible = profile === 'morning_solar' && probeEnabled && pvPowerAvg10mW >= morningProbePvAvgMinW && pvTrendW >= morningProbePvTrendMinW && soc >= morningSocMin && batteryCurrentA >= morningBatteryMaxDischargeA; const middayProbeEligible = profile === 'midday_hc_or_solar' && probeEnabled && pvPowerAvg10mW >= middayProbePvAvgMinW && soc >= middaySocMin && batteryCurrentA >= middayBatteryMaxDischargeA; // Solar Potential Condition (High Battery + Forecast) const cloudDebtOkForPotential = cloudDebtPrevKwh <= cloudDebtMaxKwh; const batteryOkForPotential = batteryCurrentA >= -Math.max(1, batteryNeutralBandA); const isHighBatteryForPotential = soc >= 85 && forecastBufferKwh > 0 && cloudDebtOkForPotential && batteryOkForPotential; const potentialAllowed = userStrategy !== 'auto_solar'; // auto_solar_plus autorise aussi const isForcePotential = userStrategy === 'force_charge_now' || userStrategy === 'force_grid_now'; const strongSolarSignal = pvPowerW >= effectivePvStartMinW || pvPowerAvg10mW >= pvHoldMinW || (departurePriorityActive && pvPowerAvg10mW >= pvHoldMinW); let sourceMode = 'blocked'; if (isForcePotential) { sourceMode = 'grid_forced'; // Priorité à l'utilisateur } else if (profile === 'midday_hc_or_solar' || profile === 'morning_solar') { if (strongSolarSignal) { sourceMode = 'solar_only'; } else if (isHighBatteryForPotential && potentialAllowed) { sourceMode = 'solar_potential'; // Le nouveau comportement clé ! } else if (profile === 'midday_hc_or_solar' && middayProbeEligible) { sourceMode = 'hybrid_probe'; } else if (allowGridHcMidday && gridIsOn) { sourceMode = 'grid_hc_allowed'; } else { sourceMode = 'blocked'; // Fallback strict si PV bridé et pas de potentiel clair } } else if (profile === 'night_hc') { sourceMode = allowGridHcNight && gridIsOn ? 'grid_hc_allowed' : 'blocked'; } else if (profile === 'evening_high_soc') { sourceMode = strongSolarSignal ? 'solar_only' : 'blocked'; } else if (profile === 'outside_window' && userStrategy !== 'auto_solar') { // Nouvelle condition : autorise charge en dehors fenêtre si stratégie auto (pas strict auto_solar) if (strongSolarSignal) { sourceMode = 'solar_only'; } else if (isHighBatteryForPotential && potentialAllowed) { sourceMode = 'solar_potential'; } } const operatingMode = profile;