// ====================================================== // EVCS SEMANTIC STATE // Ici on ne détecte PAS la voiture pleine par seuils. // On consomme seulement l'état sémantique déjà produit // par le workflow EVCS. // ====================================================== const evcsSurplusStateRaw = ( pickDefined( input.evcs_surplus_state, input.victron_evcs_surplus_state, input['input_text.victron_evcs_surplus_state'] ) ?? '' ).toString().trim(); const evcsSurplusState = low(evcsSurplusStateRaw); const evcsVehicleNoDemandSemantic = evcsSurplusState === 'vehicle_full_or_paused' || evcsSurplusState === 'vehicle_not_requesting_power' || evcsSurplusState === 'vehicle_full' || evcsSurplusState === 'charge_complete' || evcsSurplusState.includes('full_or_paused') || evcsSurplusState.includes('not_requesting_power') || evcsSurplusState.includes('charge_complete') || evcsSurplusState.includes('no_demand'); // ====================================================== // OVERRIDE EVCS VERS GRID // Si EVCS dit déjà "plus de demande", on relâche l'override. // ====================================================== const evcsGridOverride = !evcsVehicleNoDemandSemantic && ( userStrategy === 'force_grid_now' || userStrategy === 'force_charge_now' || evcsPriorityActive || evcsNightGridTakeoverNeeded || evcsSourceMode === 'réseau forcé' || evcsSourceMode === 'réseau hc autorisé' || evcsSourceMode === 'grid_forced' || evcsSourceMode === 'grid_hc_allowed' ); // ====================================================== // DÉCISION // ====================================================== let state = 'observe_only'; let reason = []; let desiredGrid = gridIsOn ? 'on' : 'off'; let desiredLatch = latchIsOn ? 'on' : 'off'; // ====================================================== // CORE HYSTERESIS LOGIC // ====================================================== if (evcsGridOverride) { desiredGrid = 'on'; desiredLatch = 'on'; if (!gridIsOn) { state = 'turn_on_evcs_override'; reason.push('Override EVCS actif : activation EDF forcée'); } else if (!latchIsOn) { state = 'adopt_existing_grid_on_evcs_override'; reason.push('Override EVCS actif : EDF déjà ON, adoption par latch'); } else { state = 'hold_on_evcs_override'; reason.push('Override EVCS actif : maintien EDF ON'); } if (userStrategy === 'force_grid_now') { reason.push('Cause override : force_grid_now'); } else if (userStrategy === 'force_charge_now') { reason.push('Cause override : force_charge_now'); } else if (evcsPriorityActive) { reason.push('Cause override : evcs_priority_active'); } else if (evcsNightGridTakeoverNeeded) { reason.push('Cause override : night_grid_takeover_needed'); } else { reason.push(`Cause override : source_mode=${evcsSourceModeRaw || 'n/a'}`); } } else if (!hysteresisEnabled) { state = 'hysteresis_disabled'; desiredGrid = gridIsOn ? 'on' : 'off'; desiredLatch = latchIsOn ? 'on' : 'off'; reason.push('Automatisme hystérésis réseau désactivé'); } else if (soc <= gridSocOn) { desiredGrid = 'on'; desiredLatch = 'on'; if (!cooldownElapsed && !gridIsOn) { state = 'turn_on_low_soc_cooldown_bypassed'; reason.push(`SOC ${round1(soc)}% <= seuil ON ${gridSocOn}% : activation EDF malgré cooldown`); } else if (!gridIsOn) { state = 'turn_on_low_soc'; reason.push(`SOC ${round1(soc)}% <= seuil ON ${gridSocOn}% : activation EDF`); } else if (!latchIsOn) { state = 'adopt_existing_grid_on_low_soc'; reason.push(`SOC ${round1(soc)}% <= seuil ON ${gridSocOn}% : EDF déjà ON, prise de contrôle par latch`); } else { state = 'hold_on_low_soc'; reason.push(`SOC encore bas ${round1(soc)}% : maintien EDF ON`); } } else if (gridIsOn && !latchIsOn) { if (soc < gridSocOff) { desiredGrid = 'on'; desiredLatch = 'on'; state = 'adopt_existing_grid_on'; reason.push(`EDF déjà ON avec latch OFF et SOC ${round1(soc)}% < seuil OFF ${gridSocOff}% : adoption de l'état ON`); } else { desiredGrid = 'off'; desiredLatch = 'off'; state = 'turn_off_stray_grid_on'; reason.push(`EDF ON avec latch OFF et SOC ${round1(soc)}% >= seuil OFF ${gridSocOff}% : coupure de reprise de contrôle`); } } else if (latchIsOn) { if (!minOnElapsed) { desiredGrid = 'on'; desiredLatch = 'on'; state = 'hold_on_min_runtime'; reason.push(`EDF maintenu ON : durée mini ${gridMinOnMinutes} min non atteinte (${round1(minutesSinceLastOn || 0)} min)`); } else if (soc < gridSocOff) { desiredGrid = 'on'; desiredLatch = 'on'; state = 'hold_on_until_soc_off'; reason.push(`EDF maintenu ON : SOC ${round1(soc)}% < seuil OFF ${gridSocOff}%`); } else { desiredGrid = 'off'; desiredLatch = 'off'; state = 'turn_off_recovered'; reason.push(`SOC ${round1(soc)}% >= seuil OFF ${gridSocOff}% : coupure EDF`); } } else { desiredGrid = 'off'; desiredLatch = 'off'; state = 'observe_only'; reason.push('Aucune action : hors zone d’hystérésis'); } // ====================================================== // COMPARE DESIRED VS CURRENT // ====================================================== const gridAction = desiredGrid === (gridIsOn ? 'on' : 'off') ? 'none' : desiredGrid === 'on' ? 'turn_on' : 'turn_off'; const latchAction = desiredLatch === (latchIsOn ? 'on' : 'off') ? 'none' : desiredLatch === 'on' ? 'turn_on' : 'turn_off'; // ====================================================== // TIMESTAMPS // ====================================================== const setLastOnAt = gridAction === 'turn_on' || state === 'adopt_existing_grid_on' || state === 'adopt_existing_grid_on_low_soc' || state === 'adopt_existing_grid_on_evcs_override'; const setLastOffAt = gridAction === 'turn_off'; // ====================================================== // CONTEXT / ALERTS // ====================================================== reason.push(`Grid actuel ${gridIsOn ? 'ON' : 'OFF'}`); reason.push(`Latch actuel ${latchIsOn ? 'ON' : 'OFF'}`); reason.push(`SOC ${round1(soc)}% | seuils ${gridSocOn}%/${gridSocOff}%`); reason.push(`PV ${Math.round(pvPowerW)}W`); reason.push(`Charge AC maison ${Math.round(houseAcPowerW)}W`); reason.push(`Puissance réseau ${Math.round(gridPowerW)}W`); reason.push(`EVCS ${evcsSeemsActive ? 'actif' : 'inactif'} | ${Math.round(evcsAcPowerW)}W | status_raw=${Number.isFinite(evcsStatusRaw) ? evcsStatusRaw : 'n/a'}`); if (minutesSinceLastOn !== null) { reason.push(`Dernier ON il y a ${round1(minutesSinceLastOn)} min`); } if (minutesSinceLastOff !== null) { reason.push(`Dernier OFF il y a ${round1(minutesSinceLastOff)} min`); } if (acOverSafeLimit) { reason.push(`⚠️ Charge AC maison ${Math.round(houseAcPowerW)}W >= limite sûre ${Math.round(acLoadsMaxSafeW)}W`); } reason.push(`EVCS override ${evcsGridOverride ? 'ON' : 'OFF'}`); reason.push(`EVCS priority ${evcsPriorityActive ? 'ON' : 'OFF'}`); reason.push(`EVCS night takeover ${evcsNightGridTakeoverNeeded ? 'ON' : 'OFF'}`); reason.push(`EVCS source ${evcsSourceModeRaw || 'n/a'}`); reason.push(`EVCS strategy ${userStrategyRaw || 'n/a'}`); reason.push(`EVCS surplus state ${evcsSurplusStateRaw || 'n/a'}`); reason.push(`EVCS no-demand semantic ${evcsVehicleNoDemandSemantic ? 'ON' : 'OFF'}`);