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:
53
HANDOFF.md
53
HANDOFF.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
209
tools/patch-20260807-p8-release-full.mjs
Normal file
209
tools/patch-20260807-p8-release-full.mjs
Normal 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
Reference in New Issue
Block a user