// Source effectivement livrable. // IMPORTANT: // on évite de conclure "voiture pleine" si en réalité la source n'est // pas disponible (ex: attente grid takeover). const sourceDeliverableForNoDemand = (context.sourceMode === 'grid_hc_allowed' || context.sourceMode === 'grid_forced') ? !!parsed.gridAvailable : (context.sourceMode === 'solar_only' || context.sourceMode === 'solar_potential') ? !!conditions.solarHoldOk || !!conditions.solarStartOk : context.sourceMode === 'hybrid_probe' ? !!conditions.probeHoldGuardOk || !!conditions.probeSuccessNow || !!conditions.probeStartOk : context.sourceMode === 'battery_assist_night' ? !!conditions.batteryAssistNightHoldOk || !!conditions.batteryAssistNightStartOk : false; const evcsSessionArmedForNoDemand = !!cloud.sessionObservedActive && !!parsed.evcsVehicleConnected && !!parsed.evcsChargingSwitchOn && !!parsed.chargePermissionNow && !!parsed.controlLatch && commandCurrentA > 0 && sourceDeliverableForNoDemand; const noDemandScoreRaw = evcsSessionArmedForNoDemand ? clamp( noDemandEvidenceRaw * sessionArmFactor * commandFactor, 0, 1 ) : 0; const noDemandScoreEma = ema(prevNoDemandScoreEma, noDemandScoreRaw, alphaScore); const adaptiveDecisionCenter = clamp( 0.72 - 0.20 * deliveryNoiseEma, 0.55, 0.82 ); const adaptiveDecisionBand = clamp( 0.16 + 0.80 * deliveryNoiseEma, 0.12, 0.34 ); const noDemandScoreOn = clamp( adaptiveDecisionCenter + adaptiveDecisionBand / 2, 0.60, 0.95 ); const noDemandScoreOff = clamp( adaptiveDecisionCenter - adaptiveDecisionBand / 2, 0.15, noDemandScoreOn - 0.05 ); let evcsVehicleNoDemandSemantic = prevNoDemandSemanticActive; if (!evcsSessionArmedForNoDemand) { evcsVehicleNoDemandSemantic = false; } else if (prevNoDemandSemanticActive) { evcsVehicleNoDemandSemantic = noDemandScoreEma >= noDemandScoreOff; } else { evcsVehicleNoDemandSemantic = noDemandScoreEma >= noDemandScoreOn; } const evcsNoDemandSinceMs = evcsVehicleNoDemandSemantic ? ( prevNoDemandSemanticActive && Number.isFinite(prevNoDemandSinceMs) ? prevNoDemandSinceMs : nowMs ) : null; const evcsNoDemandSinceMinutes = Number.isFinite(evcsNoDemandSinceMs) && Number.isFinite(nowMs) ? Math.max(0, (nowMs - evcsNoDemandSinceMs) / 60000) : null; const vehicleDemandFilter = { dtSec: round3(noDemandDtSec), commandCurrentA: round3(commandCurrentA), realCurrentA: round3(realCurrentA), expectedPowerW: round3(expectedPowerW), realPowerW: round3(realPowerW), currentDeliveryRatioRaw: round3(currentDeliveryRatioRaw), powerDeliveryRatioRaw: round3(powerDeliveryRatioRaw), deliveryRatioRaw: round3(deliveryRatioRaw), deliveryRatioEma: round3(deliveryRatioEma), deliveryNoiseEma: round3(deliveryNoiseEma), adaptiveQuietBand: round3(adaptiveQuietBand), noDemandEvidenceRaw: round3(noDemandEvidenceRaw), sessionElapsedSec: round3(sessionElapsedSec), sessionArmFactor: round3(sessionArmFactor), commandFactor: round3(commandFactor), sourceDeliverableForNoDemand, evcsSessionArmedForNoDemand, noDemandScoreRaw: round3(noDemandScoreRaw), noDemandScoreEma: round3(noDemandScoreEma), noDemandScoreOn: round3(noDemandScoreOn), noDemandScoreOff: round3(noDemandScoreOff), evcsVehicleNoDemandSemantic, evcsNoDemandSinceMs: Number.isFinite(evcsNoDemandSinceMs) ? evcsNoDemandSinceMs : null, evcsNoDemandSinceMinutes: Number.isFinite(evcsNoDemandSinceMinutes) ? round3(evcsNoDemandSinceMinutes) : null, nextHistory: { evcsDeliveryRatioEma: round3(deliveryRatioEma), evcsDeliveryNoiseEma: round3(deliveryNoiseEma), evcsNoDemandScoreEma: round3(noDemandScoreEma), evcsNoDemandSemanticActive: evcsVehicleNoDemandSemantic, evcsNoDemandSinceMs: Number.isFinite(evcsNoDemandSinceMs) ? evcsNoDemandSinceMs : null, evcsNoDemandFilterEvalMs: nowMs } }; // ===================================================== // DECISION TREE // ===================================================== if (!parsed.controllerEnabled) { desiredCharging = parsed.evcsChargingSwitchOn; desiredPermission = parsed.chargePermissionNow; desiredLatch = parsed.controlLatch; desiredCurrentA = clamp( derived.currentReferenceA || params.currentMinA, params.currentMinA, params.currentMaxAutoA ); state = 'controller_disabled'; currentDecision = 'hold'; dynamicOk = false; mustStop = false; reason.push('Contrôleur désactivé'); } else if (strategy.isStopMode) { setOff({ stateName: 'stopped_user', decisionName: 'stop_stop_mode', reasonText: 'Arrêt manuel via Stratégie STOP', dynamic: false, mustStopFlag: false, blockReason: 'manual_stop', blockedUntilStamp: now.stamp, }); } else if (parsed.evcsModeIsAuto) { setOff({ stateName: 'mode_auto_conflict', decisionName: 'stop_auto_mode_conflict', reasonText: 'Borne en mode Auto interne', dynamic: false, mustStopFlag: true, blockReason: 'mode_auto_conflict', blockedUntilStamp: now.stamp, }); } else if (!parsed.evcsDeviceConnected || !parsed.evcsVehicleConnected) { setOff({ stateName: 'vehicle_not_connected', decisionName: 'stop_not_connected', reasonText: 'Véhicule ou borne non connecté', dynamic: false, mustStopFlag: false, blockReason: 'vehicle_not_connected', blockedUntilStamp: now.stamp, }); } else if (safeguards.mustStopHard) { setOff({ stateName: derived.anyVebusLowBattery ? 'vebus_low_battery_hard_stop' : derived.vebusDcVoltageHardLow ? 'vebus_dc_voltage_hard_stop' : derived.vebusAcOutHardReached ? 'vebus_ac_hard_stop' : safeguards.batteryCurrentHardExceeded ? 'battery_current_hard_stop' : 'hard_stop', decisionName: 'stop_hard', reasonText: 'Hard stop de protection', dynamic: false, mustStopFlag: true, blockReason: safeguards.batteryCurrentHardExceeded ? 'battery_current_hard_stop' : derived.vebusDcVoltageHardLow ? 'vebus_dc_voltage_stop' : derived.anyVebusLowBattery ? 'vebus_low_battery_stop' : derived.vebusAcOutHardReached ? 'vebus_ac_hard_stop' : 'battery_protection_stop', blockedUntilStamp: formatParisStampFromMs(nowMs + 15 * 60000), }); } // ===================================================== // ACTIVE SESSION BRANCH // ===================================================== else if (cloud.sessionObservedActive) { if (cloud.enteringCloudDebt) { setCloudDebtStartedAt = true; } if (conditions.shouldStopOverloadAtMin) { setOff({ stateName: 'vebus_overload_stop_min_current', decisionName: 'stop', reasonText: 'Arrêt: surcharge convertisseur persistante au courant mini', dynamic: false, mustStopFlag: true, blockReason: 'vebus_overload_persistent', blockedUntilStamp: formatParisStampFromMs( nowMs + batteryTune.global.overloadStopAfterSeconds * 1000 ), }); } else if (safeguards.gridBatteryProtectionStop) { setOff({ stateName: 'stop_grid_battery_guard', decisionName: 'stop', reasonText: `Arrêt charge réseau: batterie trop sollicitée (${round1(parsed.batteryCurrentA)}A)`, dynamic: false, mustStopFlag: true, blockReason: 'grid_battery_guard', blockedUntilStamp: formatParisStampFromMs(nowMs + 20 * 60000), }); } else if (conditions.probeFailedNow) { setOff({ stateName: 'probe_failed_lock', decisionName: 'stop', reasonText: 'Mode sonde échoué', dynamic: false, mustStopFlag: true, blockReason: 'probe_failed', blockedUntilStamp: formatParisStampFromMs(nowMs + params.probeRetryMinutes * 60000), }); } else if (derived.physicalCapA < params.currentMinA) { if (lifecycle.minRunElapsed) { setOff({ stateName: 'stop_power_cap', decisionName: 'stop_power_cap', reasonText: 'Capacité système insuffisante', dynamic: false, mustStopFlag: true, blockReason: 'system_capacity_stop', blockedUntilStamp: formatParisStampFromMs(nowMs + 10 * 60000), }); } else { setOn({ stateName: 'hold_min_run_power_cap', decisionName: 'hold', currentA: params.currentMinA, reasonText: "Maintien jusqu'au temps mini ON", dynamic: false, mustStopFlag: false, }); } } else if (conditions.probeWindowActive && !conditions.probeSuccessNow) { if (!conditions.probeHoldGuardOk) { setOff({ stateName: 'probe_stop_guard', decisionName: 'stop', reasonText: 'Sonde interrompue: garde-fous non respectés', dynamic: false, mustStopFlag: true, blockReason: 'probe_failed', blockedUntilStamp: formatParisStampFromMs(nowMs + params.probeRetryMinutes * 60000), }); } else { setOn({ stateName: 'probe_hold', decisionName: 'hold', currentA: Math.min(params.probeCurrentA, derived.physicalCapA), reasonText: `Mode sonde actif à ${round1(Math.min(params.probeCurrentA, derived.physicalCapA))}A`, dynamic: true, mustStopFlag: false, }); } } else { const activeSourceMode = conditions.probeSuccessNow ? 'solar_only' : context.sourceMode; const holdConditionsOk = (activeSourceMode === 'grid_hc_allowed' || activeSourceMode === 'grid_forced') ? conditions.gridHoldOk : (activeSourceMode === 'solar_only' || activeSourceMode === 'solar_potential') ? conditions.solarHoldOk : activeSourceMode === 'hybrid_probe' ? conditions.probeHoldGuardOk : activeSourceMode === 'battery_assist_night' ? conditions.batteryAssistNightHoldOk : false;