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

@@ -572,3 +572,56 @@ takeoverNeeded, action EDF ON, service helper takeover.
--- ---
*Document mis à jour le : 07/08/2026 (~19:20) — R3 en place, 16/16, push bloquant* *Document mis à jour le : 07/08/2026 (~19:20) — R3 en place, 16/16, push bloquant*
## 14. Session du 07/08/2026 (soir) — P8 : Libération EDF quand le véhicule est plein
### Spécification utilisateur
La nuit : charge dès 01:24 (P7) ✓, puis **quand la charge est finie → EDF OFF,
sauf si la batterie solaire est sous le seuil** (alors EDF reste pour la recharger).
### Bugs découverts en traçant la spec
1. **Fix 04/08 logiquement inversé** : `(soc < 90 || vehicleChargeCompleted)` GARDAIT
le latch takeover après la charge au lieu de le libérer. Jamais corrigé en réalité.
2. **P7 ne relâchait pas** : `nightGridHcPreferred` restait vrai tant que véhicule
branché → l'EVCS redemandait EDF ON toutes les 30 s jusqu'à 07:24.
3. **Signal "voiture pleine" instable** : le VehicleDemandFilter retombe à false dès
que le relais s'ouvre (session désarmée) → impossible de s'appuyer dessus seul.
### Solution : mémoire AUTO-ENTRETENUE via l'état surplus (P8)
Chaîne (nouveau champ snapshot `prev_surplus_state` ← input_text.victron_evcs_surplus_state) :
```
charge → VDF détecte voiture pleine → state 'vehicle_full_or_paused'
cycle N+1 : WINDOWS voit ce state → vehicleChargeCompleted=true
→ DECISION émet 'night_session_done_idle'
cycles suivants : 'night_session_done_idle' s'auto-entretient
fin : débranchement (state vehicle_not_connected) ou fin de fenêtre 07:24
```
Effets quand vehicleChargeCompleted :
- latch libéré (fix inversion 04/08 : `soc < 90 && !completed`)
- préférence P7 relâchée ; sourceMode nuit → 'blocked' (zéro tentative de charge)
- SOC > cible matin : takeover helper OFF → hystérésis coupe EDF (turn_off_recovered)
- SOC ≤ cible : takeover helper ON → hystérésis maintient EDF (override) + ACM
recharge la batterie à la cible → puis relâche. ⚠️ Le nom 'night_session_done_idle'
ne doit matcher AUCUN pattern no-demand de l'hystérésis (full_or_paused /
not_requesting_power / charge_complete / no_demand) — c'est ce qui permet
à l'override batterie-basse de fonctionner. NE PAS RENOMMER sans vérifier.
### Nœuds modifiés (tools/patch-20260807-p8-release-full.mjs, rejouable)
- Snapshot template : +prev_surplus_state
- WINDOWS_SOURCEV2 : vehicleChargeCompleted collant, latch corrigé, P7 conditionné,
preferred 'blocked' si plein, export preview
- BATTERY_CLOUD_LOCKS : passthrough du flag dans nightEvcs
- DECISION_ENGINEV2 : branche 'night_session_done_idle' (avant sourceMode blocked)
### Validation
- 3 nouveaux scénarios golden (19 au total) : transition vehicle_full→done_idle
(takeover turn_off), auto-entretien, batterie basse (takeover turn_on maintenu).
- Les 16 scénarios existants inchangés (aucune régression).
- Push via pipeline bloquant (golden 19/19), production stable.
### Comportement attendu cette nuit (VW branchée, checklist complète)
1. 01:24-01:25 : grid_hc_allowed → EDF ON → charge 6 A puis montée (P7)
2. VW pleine : vehicle_full_or_paused → night_session_done_idle → takeover OFF
→ EDF coupé par l'hystérésis ≤ 1 min après (si min_on 25 écoulé et SOC ≥ 30)
3. Batterie sous cible matin (35 été) : EDF maintenu + recharge ACM jusqu'à cible
4. 07:24 : fin de fenêtre → retour profil jour

View File

@@ -88,6 +88,7 @@ function baseOverrides(fixedMs) {
probe_started_at: '', probe_started_at: '',
cloud_debt_kwh: '0.0', cloud_debt_kwh: '0.0',
night_takeover_prev: 'off', night_takeover_prev: 'off',
prev_surplus_state: 'idle_wait_conditions',
grid_state: 'off', grid_state: 'off',
victron_acin1_available: '0', victron_acin1_available: '0',
evcs_charging_switch: 'off', evcs_charging_switch: 'off',

View File

@@ -135,6 +135,75 @@ export const scenarios = [
}, },
}, },
// ================= FIN DE CHARGE NUIT (P8) =================
{
id: 'night-vw-pleine-transition',
label: 'Nuit HC 04:00, cycle après vehicle_full_or_paused → veille done_idle, takeover relâché',
time: '2026-08-08T04:00:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '60', allow_grid_hc_night: 'on',
grid_state: 'on', victron_acin1_available: '1',
battery_current_a: '2', night_takeover_prev: 'on',
evcs_charging_switch: 'off',
prev_surplus_state: 'vehicle_full_or_paused',
},
expect: {
profile: 'night_hc',
sourceMode: 'blocked',
state: 'night_session_done_idle',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'night-vw-pleine-autoentretien',
label: 'Nuit HC 05:00, veille done_idle auto-entretenue, EDF déjà coupé → reste en veille',
time: '2026-08-08T05:00:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '55', allow_grid_hc_night: 'on',
evcs_charging_switch: 'off',
prev_surplus_state: 'night_session_done_idle',
},
expect: {
profile: 'night_hc',
sourceMode: 'blocked',
state: 'night_session_done_idle',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'night-vw-pleine-batterie-basse-edf-maintenu',
label: 'Nuit HC 05:00, VW pleine MAIS SOC 30 ≤ cible 35 → takeover maintenu (recharge batterie), EVCS en veille',
time: '2026-08-08T05:00:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '30', allow_grid_hc_night: 'on',
grid_state: 'on', victron_acin1_available: '1',
battery_current_a: '2', night_takeover_prev: 'on',
evcs_charging_switch: 'off',
prev_surplus_state: 'night_session_done_idle',
},
expect: {
profile: 'night_hc',
sourceMode: 'blocked',
state: 'night_session_done_idle',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: true,
edfOnAction: false,
takeoverHelperService: 'turn_on',
},
},
// ================= JOUR SOLAIRE ================= // ================= JOUR SOLAIRE =================
{ {
id: 'morning-solaire-fort', id: 'morning-solaire-fort',

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);

File diff suppressed because one or more lines are too long