Fix E: latch 'charge terminée' en session active + coupure recovery_done (incident rebond 08-09/08)

- DECISION: branche completed prioritaire dans la session active (avant VDF/source-lost)
  → état collant écrit au 1er cycle status=3, relais encore fermé
- WINDOWS: garde puissance <=300W sur Fix D (anti-glitch)
- Hystérésis: turn_off_recovery_done (bypass min_on, dry-run 5 cas)
- golden 23→25 (session armée status 3, flap statut post-latch)
- HANDOFF §18
This commit is contained in:
Tallagrand Thierry
2026-08-09 06:31:33 +02:00
parent 9bc4af0ea1
commit b287e5ba73
7 changed files with 225 additions and 5 deletions

View File

@@ -0,0 +1,77 @@
#!/usr/bin/env node
// Patch 09/08/2026 — FIX E2 (relais) : coupure EDF immédiate quand l'état
// surplus est 'night_recovery_done_idle' (collant : le takeover ne peut plus
// revenir) et qu'aucun override n'est actif. Évite les 25 min de
// hold_on_min_runtime après la tentative de charge d'un véhicule déjà plein.
// 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_recovery_done')) {
console.log('Déjà appliqué — rien à faire.');
process.exit(0);
}
const anchor = ` state = 'turn_off_vehicle_unplugged';
reason.push('Vehicule debranche + SOC >= seuil OFF : coupure immediate du reseau (bypass min_on)');
forcedCut = true;
}
`;
const insertion = ` state = 'turn_off_vehicle_unplugged';
reason.push('Vehicule debranche + SOC >= seuil OFF : coupure immediate du reseau (bypass min_on)');
forcedCut = true;
}
// ======================================================
// FIX E2 (09/08/2026): RECOVERY DONE -> COUPURE IMMEDIATE
// 'night_recovery_done_idle' est un etat COLLANT du controleur EVCS :
// charge terminee ET batterie a la cible, le takeover ne reviendra pas
// (sauf SOC <= cible-2, qui repasserait par l'override). Attendre le
// min_on 25 min ne protege ici de rien : coupure immediate.
// ======================================================
if (
!forcedCut &&
latchIsOn &&
gridIsOn &&
!evcsGridOverride &&
evcsSurplusState === 'night_recovery_done_idle' &&
soc >= gridSocOff
) {
desiredGrid = 'off';
desiredLatch = 'off';
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 turn_off_vehicle_unplugged introuvable — appliquer d abord patch-20260808-c-unplug-cut.mjs');
code = code.replace(anchor, insertion);
const notifAnchor = `} else if (state === 'turn_off_recovered') {`;
const notifInsertion = `} else if (state === 'turn_off_recovery_done') {
notifyTitle = '✅ EDF coupé (charge terminée)';
notifyMessage = \`Charge VE terminée et batterie à la cible (SOC \${round1(soc)}%). Coupure immédiate du réseau.\`;
notifyWanted = true;
} else if (state === 'turn_off_recovered') {`;
if (!code.includes(notifAnchor)) throw new Error('Ancre notification turn_off_recovered introuvable');
code = code.replace(notifAnchor, notifInsertion);
node.parameters.jsCode = code;
fs.writeFileSync(FILE, JSON.stringify(wf, null, 2) + '\n');
console.log('Patch appliqué →', FILE, `(jsCode ${code.length} chars)`);