Files
EVCS/archive/HYSTERESIS/COMPUTEP2.js
Tallagrand Thierry 1ae8129658 R2: git = source de vérité — src/nodes/ + build/extract intégrés au push
- src/nodes/*.js (6 nodes + template snapshot) = source de vérité unique.
- n8n-push.mjs reconstruit le JSON depuis src/ avant syntaxe+golden+PUT
  (src gagne toujours; n8n-extract.mjs pour resynchroniser si édition UI).
- n8n-build.mjs --check pour vérifier la synchro.
- Copies historiques déplacées dans archive/ (mortes, dérivées de la prod).
- .gitignore: backups locaux de rollback.
- AGENTS.md: règle R2 pour toutes les futures sessions (dont Qwen local).
- Validé: idempotence, mutation/restauration, pipeline push complet en réel.
2026-08-07 21:03:52 +02:00

202 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ======================================================
// 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'}`);