Fix G: coupure EDF générique quand l'override EVCS disparaît mi-session

Incident nuit 21-22/08 : la fenêtre HC nuit se ferme pendant que le véhicule
est encore en session active (pas encore flag "charge terminée"). L'EVCS
relâche correctement son override, mais émet un état transitoire générique
(stop_source_lost/idle_wait_source) qu'aucune règle de coupure immédiate du
workflow Grid Hysteresis (Fix C/E2) ne reconnaît. EDF restait alors maintenu
des heures par la boucle générique jusqu'au seuil statique gridSocOff,
plafonnant le MPPT Victron à ~100W et privant la maison de solaire.

Nouvelle règle générique (Compute Grid Hysteresis) basée sur evcsGridOverride
plutôt que sur une liste blanche de noms de surplus_state — couvre donc aussi
tout futur état non énuméré. Garde anti-glitch sur lecture HA invalide.
Nouvelle suite tests/golden-hysteresis/ (7 scénarios, première pour ce
workflow), câblée bloquante dans tools/n8n-push.mjs. Déployé et validé
(golden-hysteresis 7/7, golden EVCS 39/39 inchangé).

Écarte le diagnostic incomplet laissé par un autre agent dans HANDOFF.md
(edf_general_ok_off côté EVCS aurait créé un deuxième écrivain non coordonné
sur le même switch).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Tallagrand Thierry
2026-08-22 19:56:06 +02:00
parent 5bc9784583
commit e68fa160b6
8 changed files with 577 additions and 5 deletions

View File

@@ -0,0 +1,99 @@
#!/usr/bin/env node
// Patch 22/08/2026 — FIX G (relais) : coupure EDF immédiate, générique, dès
// que l'override EVCS disparaît (evcsGridOverride === false) et que le SOC
// est au-dessus du seuil bas ON (gridSocOn, PAS gridSocOff) — au lieu de
// dépendre d'une liste blanche de noms exacts de surplus_state (Fix C/E2),
// qui ratait les états transitoires génériques (stop_source_lost,
// idle_wait_source) émis quand la fenêtre HC nuit se ferme mi-session.
// Incident : nuit 21-22/08/2026, EDF resté ON des heures après la fermeture
// de la fenêtre HC, PV plafonné ~100W par le MPPT.
// Cible : workflows/victron-grid-hysteresis-v1.json (node "Compute Grid Hysteresis").
// Rejouable : détecte si déjà appliqué.
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const FILE = path.join(ROOT, 'workflows/victron-grid-hysteresis-v1.json');
const wf = JSON.parse(fs.readFileSync(FILE, 'utf8'));
const node = wf.nodes.find(n => n.name === 'Compute Grid Hysteresis');
if (!node) throw new Error('Node "Compute Grid Hysteresis" introuvable');
let code = node.parameters.jsCode;
if (code.includes('turn_off_evcs_override_released')) {
console.log('Déjà appliqué — rien à faire.');
process.exit(0);
}
const anchor = ` state = 'turn_off_recovery_done';
reason.push('Charge terminee + batterie a la cible : coupure immediate du reseau (bypass min_on)');
forcedCut = true;
}
`;
if (!code.includes(anchor)) {
throw new Error('Ancre Fix E2 introuvable — le node a peut-être changé, vérifier manuellement.');
}
const insertion = `${anchor}
// ======================================================
// FIX G (22/08/2026): OVERRIDE EVCS DISPARU -> COUPURE IMMEDIATE
// Incident nuit 21-22/08 : a la fermeture de la fenetre HC (~06-07h), le
// controleur EVCS remet correctement son override a false MAIS le vehicule
// n'est pas encore flag "charge terminee" -> DECISION_ENGINEV2 retombe sur
// des etats generiques transitoires (stop_source_lost, idle_wait_source,
// et potentiellement d'autres non enumeres ici). Fix C/E2 ne matchent
// aucun de ces noms -> EDF restait maintenu ON par la boucle generique
// jusqu'a gridSocOff (35%), potentiellement des heures.
//
// Regle GENERIQUE (pas un whitelist de plus) : des que evcsGridOverride
// redevient false ET SOC > gridSocOn (22%, PAS gridSocOff 35% : on ne
// touche pas au cas independant "SOC bas" qui doit tenir jusqu'a
// gridSocOff), on coupe immediatement.
//
// Garde anti-glitch : n'agit pas si la lecture evcs_source_mode est
// vide/unavailable/unknown (panne HA transitoire) - dans ce cas la boucle
// generique gere normalement plutot que de couper sur une lecture non fiable.
// ======================================================
const evcsSourceModeLooksValid =
evcsSourceModeRaw !== '' &&
!['unavailable', 'unknown'].includes(low(evcsSourceModeRaw));
if (
!forcedCut &&
latchIsOn &&
gridIsOn &&
!evcsGridOverride &&
soc > gridSocOn &&
evcsSourceModeLooksValid
) {
desiredGrid = 'off';
desiredLatch = 'off';
state = 'turn_off_evcs_override_released';
reason.push(\`Override EVCS disparu (source=\${evcsSourceModeRaw || 'n/a'}, surplus_state=\${evcsSurplusStateRaw || 'n/a'}) + SOC \${round1(soc)}% > seuil ON \${gridSocOn}% : coupure immediate du reseau (bypass min_on, libere le solaire)\`);
forcedCut = true;
}
`;
code = code.replace(anchor, insertion);
const notifAnchor = `} else if (state === 'turn_off_recovered') {`;
if (!code.includes(notifAnchor)) {
throw new Error('Ancre notification turn_off_recovered introuvable.');
}
const notifInsertion = `} else if (state === 'turn_off_evcs_override_released') {
notifyTitle = '✅ EDF coupé (override EVCS disparu)';
notifyMessage = \`L'override EVCS a disparu (source=\${evcsSourceModeRaw || 'n/a'}) et SOC \${round1(soc)}% > seuil ON \${gridSocOn}%. Coupure immédiate du réseau pour libérer le solaire.\`;
notifyWanted = true;
${notifAnchor}`;
code = code.replace(notifAnchor, notifInsertion);
node.parameters.jsCode = code;
fs.writeFileSync(FILE, JSON.stringify(wf, null, 2) + '\n', 'utf8');
console.log('Fix G appliqué à', FILE);