- 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).
210 lines
7.7 KiB
JavaScript
210 lines
7.7 KiB
JavaScript
#!/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);
|