P8: libération EDF quand véhicule plein (nuit HC) + fix latch inversé du 04/08

- vehicleChargeCompleted COLLANT via prev_surplus_state (nouveau champ snapshot):
  vehicle_full_or_paused -> night_session_done_idle auto-entretenu jusqu'au
  débranchement ou fin de fenêtre.
- Fix inversion 04/08: le latch takeover se libère à la fin de charge
  (était: || vehicleChargeCompleted qui le GARDAIT).
- P7 relâché quand charge finie; sourceMode nuit -> blocked (zéro retry).
- SOC > cible: takeover OFF -> hystérésis coupe EDF. SOC <= cible: takeover ON
  -> EDF maintenu + ACM recharge batterie (spec 'sauf si sous seuil').
- night_session_done_idle ne matche aucun pattern no-demand de l'hystérésis
  (condition de fonctionnement de l'override batterie basse).
- Golden: 19/19 (3 nouveaux scénarios P8, 16 existants inchangés).
This commit is contained in:
Tallagrand Thierry
2026-08-07 19:40:30 +02:00
parent d1efeac44c
commit b10c90c74f
5 changed files with 336 additions and 4 deletions

View File

@@ -0,0 +1,209 @@
#!/usr/bin/env node
// P8 v2 (07/08/2026) : libération du réseau HC quand le véhicule est plein — version COLLANTE.
//
// Problème du signal naïf : le VehicleDemandFilter retombe à false dès que le relais
// s'ouvre (session désarmée) → un seul cycle de release puis redémarrage en boucle.
// Solution : mémoire auto-entretenue via l'état surplus relu au cycle suivant :
// charge → VDF détecte voiture pleine → state 'vehicle_full_or_paused'
// cycle N+1 : WINDOWS voit ce state → vehicleChargeCompleted → DECISION émet
// 'night_session_done_idle' → cycles suivants : ce state s'auto-entretient
// jusqu'à débranchement (state vehicle_not_connected) ou fin de fenêtre.
//
// Effets :
// - latch takeover libéré (fix de l'inversion du 04/08)
// - préférence HC (P7) relâchée
// - sourceMode nuit → 'blocked' (plus aucune tentative de charge)
// - takeover helper OFF si SOC > cible matin → l'hystérésis coupe EDF
// - takeover helper ON si SOC ≤ cible → EDF maintenu + ACM recharge la batterie
// (le nom 'night_session_done_idle' ne matche AUCUN pattern no-demand de
// l'hystérésis, donc l'override batterie-basse reste fonctionnel)
import fs from 'fs';
const FILE = 'workflows/evcs-surplus-controller-v2.6.json';
const wf = JSON.parse(fs.readFileSync(FILE, 'utf8'));
function getNode(name) {
const n = wf.nodes.find(x => x.name === name);
if (!n) throw new Error(`Node introuvable: ${name}`);
return n;
}
function replaceExact(code, from, to, label) {
if (!code.includes(from)) throw new Error(`Pattern introuvable (${label})`);
if (code.split(from).length !== 2) throw new Error(`Pattern non unique (${label})`);
return code.replace(from, to);
}
// =====================================================
// A. SNAPSHOT TEMPLATE : relire son propre état surplus
// =====================================================
const snap = getNode('HA Snapshot EVCS Surplus Controller V2.2');
const tplParam = snap.parameters.bodyParameters.parameters.find(p => p.name === 'template');
tplParam.value = replaceExact(
tplParam.value,
`"night_takeover_prev": states("input_boolean.victron_evcs_night_grid_takeover_needed"),`,
`"night_takeover_prev": states("input_boolean.victron_evcs_night_grid_takeover_needed"),
"prev_surplus_state": states("input_text.victron_evcs_surplus_state"),`,
'A template prev_surplus_state'
);
console.log('OK: template snapshot (+prev_surplus_state)');
// =====================================================
// B. WINDOWS_SOURCEV2
// =====================================================
const wNode = getNode('WINDOWS_SOURCEV2');
let w = wNode.parameters.jsCode;
// B1. vehicleChargeCompleted collant + fix latch inversé (bornes par index :
// le bloc 04/08 contient des espaces traînants)
const startAnchor = 'const vehicleChargeCompleted =';
const endAnchor = 'const nightGridHcPreferred =';
const s = w.indexOf(startAnchor), e = w.indexOf(endAnchor);
if (s === -1 || e === -1 || e < s) throw new Error('B1: bornes introuvables');
w = w.slice(0, s) + `// P8 (07/08/2026) : "charge terminée" COLLANT via l'état surplus du cycle
// précédent (helper input_text.victron_evcs_surplus_state relu par le snapshot).
// Chaîne : VDF stoppe la charge (state vehicle_full_or_paused) → ce flag s'allume
// → DECISION émet night_session_done_idle → le flag s'auto-entretient jusqu'au
// débranchement (state vehicle_not_connected) ou à la fin de la fenêtre nuit.
const prevSurplusState = (input.prev_surplus_state ?? '').toString().trim().toLowerCase();
const vehicleChargeCompleted =
nightSessionActive &&
(
prevSurplusState === 'vehicle_full_or_paused' ||
prevSurplusState === 'night_session_done_idle'
);
// P8 : correction du fix 04/08 (logique inversée : "|| vehicleChargeCompleted"
// GARDAIT le latch après la charge au lieu de le libérer).
const nightTakeoverLatched =
nightSessionActive &&
nightTakeoverPrev &&
parsed.soc < NIGHT_TAKEOVER_RELEASE_SOC_PCT &&
!vehicleChargeCompleted;
` + w.slice(e);
// B2. La préférence HC (P7) tombe quand la charge est terminée
w = replaceExact(w,
`const nightGridHcPreferred =
nightSessionActive &&
params.allowGridHcNight;`,
`const nightGridHcPreferred =
nightSessionActive &&
params.allowGridHcNight &&
!vehicleChargeCompleted;`,
'B2 préférence P7'
);
// B3. sourceMode nuit : véhicule plein → 'blocked' (aucune tentative de charge)
w = replaceExact(w,
`const nightPreferredSourceMode =
!nightSessionActive
? 'blocked'
: batteryAssistAllowed
? 'battery_assist_night'`,
`const nightPreferredSourceMode =
!nightSessionActive
? 'blocked'
: vehicleChargeCompleted
? 'blocked'
: batteryAssistAllowed
? 'battery_assist_night'`,
'B3 preferred blocked'
);
// B4. Export du flag dans le preview (consommé par BCL → DECISION)
w = replaceExact(w,
` batteryAssistAllowed,
nightGridTakeoverNeeded,
nightTakeoverLatched,
nightGridHcPreferred,
nightPreferredSourceMode,
}`,
` batteryAssistAllowed,
nightGridTakeoverNeeded,
nightTakeoverLatched,
nightGridHcPreferred,
vehicleChargeCompleted,
nightPreferredSourceMode,
}`,
'B4 export preview'
);
wNode.parameters.jsCode = w;
console.log('OK: WINDOWS_SOURCEV2 (B1-B4)');
// =====================================================
// C. BATTERY_CLOUD_LOCKS : passer le flag dans nightEvcs
// =====================================================
const bNode = getNode('BATTERY_CLOUD_LOCKS');
let b = bNode.parameters.jsCode;
b = replaceExact(b,
`const nightTakeoverLatched = !!nightPrev.nightTakeoverLatched;`,
`const nightTakeoverLatched = !!nightPrev.nightTakeoverLatched;
const vehicleChargeCompleted = !!nightPrev.vehicleChargeCompleted;`,
'C1 consume'
);
b = replaceExact(b,
` batteryAssistAllowed,
nightGridTakeoverNeeded,
nightTakeoverLatched,
nightPreferredSourceMode,
},`,
` batteryAssistAllowed,
nightGridTakeoverNeeded,
nightTakeoverLatched,
vehicleChargeCompleted,
nightPreferredSourceMode,
},`,
'C2 export nightEvcs'
);
bNode.parameters.jsCode = b;
console.log('OK: BATTERY_CLOUD_LOCKS (C1-C2)');
// =====================================================
// D. DECISION_ENGINEV2 : état auto-entretenu night_session_done_idle
// (⚠️ le nom ne doit matcher AUCUN pattern no-demand de l'hystérésis :
// full_or_paused / not_requesting_power / charge_complete / no_demand)
// =====================================================
const dNode = getNode('DECISION_ENGINEV2');
let d = dNode.parameters.jsCode;
d = replaceExact(d,
` else if (context.sourceMode === 'blocked') {
setOff({
stateName:
context.profile === 'outside_window'
? 'blocked_outside_window'`,
` // P8 : véhicule plein en session nuit → veille auto-entretenue.
// Le state est relu au cycle suivant (prev_surplus_state) : il maintient
// vehicleChargeCompleted jusqu'au débranchement ou à la fin de fenêtre.
else if (context.profile === 'night_hc' && nightEvcs?.vehicleChargeCompleted) {
setOff({
stateName: 'night_session_done_idle',
decisionName: 'hold',
reasonText: 'Charge véhicule terminée : veille nuit, réseau relâché (recharge batterie si sous cible)',
dynamic: false,
mustStopFlag: false,
blockReason: 'none',
blockedUntilStamp: now.stamp,
});
}
else if (context.sourceMode === 'blocked') {
setOff({
stateName:
context.profile === 'outside_window'
? 'blocked_outside_window'`,
'D1 branche done_idle'
);
dNode.parameters.jsCode = d;
console.log('OK: DECISION_ENGINEV2 (D1)');
fs.writeFileSync(FILE, JSON.stringify(wf, null, 2) + '\n');
console.log('Écrit:', FILE);