Fixes A/B/C/D incident rebond EDF nuit 07-08/08 + GRAFCET doc

- A: état collant night_recovery_done_idle + bande cible−2 (anti-rebond takeover)
- B: snapshot relit night_session_start_soc/at (fin des réécritures 30s)
- C: hystérésis grid — turn_off_vehicle_unplugged (coupure immédiate, bypass min_on)
- D: evcs_status_raw==3 (charged) = fin de charge immédiate
- golden 19→23 scénarios (2 attendus mis à jour, 4 nouveaux)
- MANUEL_COMPLET §11.6 GRAFCET cycle nuit, HANDOFF §17, vision-notes incident
This commit is contained in:
Tallagrand Thierry
2026-08-08 14:20:21 +02:00
parent b20b3b19fc
commit 9bc4af0ea1
11 changed files with 874 additions and 15 deletions

View File

@@ -732,3 +732,72 @@ haiku-4.5 ~9€/mois plein temps). Implémentation ~30 min, tests advisor inchan
→ Alors premier câblage réel LIMITÉ : night_target_soc uniquement, derrière un → Alors premier câblage réel LIMITÉ : night_target_soc uniquement, derrière un
nouveau toggle `advisor_apply_night_target`, le reste inchangé. nouveau toggle `advisor_apply_night_target`, le reste inchangé.
## 17. Session du 08/08/2026 — Incident rebond EDF nuit 07-08/08 : fixes A/B/C/D
### Incident (captures HA analysées, faits dans docs/vision-notes.md)
P7 a chargé la VW dès 01:24 ✓ (session 1h32m). Ensuite :
1. Fin de charge détectée très tardivement (~04:15 au lieu de ~02:56) ;
2. Rebond EDF ON/OFF période ~10 min autour de SOC=35 (= cible matin) + flapping
ACM Charging/passthru 2-4 s (03:58→04:16) ;
3. Débranchement VW (04:17) → takeover bien relâché mais EDF maintenu
(hold_on_min_runtime 25 min) → arrêt manuel utilisateur 04:25 (stopped_user).
### Root causes
- **A — point dégénéré SOC==cible** (WINDOWS_SOURCEV2) : engage takeover
`soc <= cible`, release `soc > cible`, recovery ACM `soc < cible` (strict),
et l'ACM recharge PILE à la cible → aucune position stable, oscillation.
- **B — snapshot incomplet** : `evcs_night_session_start_soc/_at` jamais relus
(absents du template) → `nightSessionNeedsCapture` vrai à CHAQUE cycle →
"Night Session Start At" réécrit toutes les 30 s toute la nuit (visible logs),
budget décharge nuit faussé (start SOC glissant).
- **C — hystérésis grid** : pas de coupure anticipée au débranchement
(min_on 25 min s'applique même véhicule parti et SOC ≥ seuil OFF).
- **D — détection fin de charge fragile** : VDF armé uniquement session
pleinement active ; stop par garde (ex battery_current_hard_stop) → filtre
désarmé → 'vehicle_full_or_paused' inatteignable pendant longtemps.
### Fixes déployés (production 08/08 ~14:00, golden 23/23)
- **Fix A** (WINDOWS_SOURCEV2 + BCL passthrough + DECISION) : nouvel état
auto-entretenu **`night_recovery_done_idle`** émis quand
vehicleChargeCompleted && SOC ≥ cible. Takeover relâché DÉFINITIVEMENT ;
ré-engagement uniquement si SOC ≤ cible − 2 (bande NIGHT_RECOVERY_REARM_BAND_PCT).
⚠ Le nom ne matche aucun pattern no-demand de l'hystérésis (comme done_idle).
Structure takeover : completed ? !nightRecoveryDone : (trigger || urgent || latch).
- **Fix B** (HA_SNAPSHOT_TEMPLATE) : +`evcs_night_session_start_soc`,
+`evcs_night_session_start_at` (helpers input_text existants, vérifiés via MCP).
- **Fix C** (workflow hystérésis 7iIlu7LLTW7TZA8o, patch
tools/patch-20260808-c-unplug-cut.mjs rejouable) : nouvel état
**`turn_off_vehicle_unplugged`** = véhicule débranché (es=vehicle_not_connected)
+ latch ON + grid ON + pas d'override + SOC ≥ seuil OFF → coupure immédiate
(bypass min_on) + notification. Dry-run 4 cas validé (incident/SOC bas/
takeover ON/recovery_done) AVANT push. Le fichier local du workflow est
désormais suivi : workflows/victron-grid-hysteresis-v1.json.
- **Fix D** (WINDOWS_SOURCEV2) : `evcs_status_raw == 3` (Victron "charged")
vaut fin de charge immédiate, sans attendre le VDF.
### Golden : 19 → 23 scénarios
- 2 attendus mis à jour (transition/auto-entretien : SOC ≥ cible → état
`night_recovery_done_idle` désormais) ;
- 4 nouveaux : `night-recovery-atteinte-soc-cible` (LE cas du rebond),
`night-recovery-done-bande-collante` (34 % dans la bande),
`night-recovery-rearm-sous-bande` (32 % → ré-engagement),
`night-vw-statut-charged` (Fix D).
- Piège outillage : `tests/golden/run.mjs` exécute le JSON BUILDÉ
(workflows/*.json), pas src/ → toujours `node tools/n8n-build.mjs` avant un
run golden manuel (n8n-push le fait automatiquement).
### Documentation
- MANUEL_COMPLET.md §11.6 : GRAFCET complet du cycle nuit (G1 EVCS / G2
hystérésis) + états surplus fin de nuit + résumé incident.
- docs/vision-notes.md : OCR/faits des 9 captures de l'incident.
### À surveiller (prochaine nuit HC, VW branchée)
1. Fin de charge : passage à vehicle_full_or_paused OU night_recovery_done_idle
rapide (Fix D si la VW publie le statut 3) ;
2. SOC à la cible → UNE coupure EDF, puis plus aucun ON/OFF (sauf SOC ≤ cible−2) ;
3. "Night Session Start At" écrit UNE fois en début de session (Fix B) ;
4. Débranchement → EDF OFF ≤ 1 min (turn_off_vehicle_unplugged, Fix C).
---
*Document mis à jour le : 08/08/2026 (~14:15) — fixes A/B/C/D déployés, golden 23/23*

View File

@@ -344,6 +344,64 @@ S'assurer que le véhicule est chargé avant l'heure de départ prévue.
- **gridTakeoverTriggerPct** : Seuil de basculement réseau - **gridTakeoverTriggerPct** : Seuil de basculement réseau
- **batteryRecoveryUrgent** : Si temps restant < temps de charge nécessaire - **batteryRecoveryUrgent** : Si temps restant < temps de charge nécessaire
### 11.6 Cycle nuit HC complet — GRAFCET (P7 + P8 + fixes 08/08/2026)
Deux automates coordonnés via les helpers HA (`night_grid_takeover_needed`,
`victron_evcs_surplus_state`) :
- **G1** = contrôleur EVCS V2.6 (boucle 30 s)
- **G2** = hystérésis EDF (« Victron - Battery / Grid Hysteresis Fix V1 », 1 min)
```
G1 — SESSION NUIT EVCS
──────────────────────
X0 JOUR / attente (profils solaires)
│ t1: fenêtre HC (01:24) ET véhicule branché
X1 SESSION NUIT ACTIVE — capture startSOC/startAt (une seule fois, Fix B)
│ t2: toggle "Réseau HC nuit" ON → nightGridHcPreferred (P7)
X2 TAKEOVER: helper ON → G2 force EDF ON ; ACM tient la batterie (~2 A)
│ t3: EDF ON confirmé (gridAvailable)
X3 CHARGE VE: start 6 A → step-up (plafond 20 A / boost 28 A)
│ t4: fin de charge = VDF no-demand OU statut EVCS brut 3 "charged" (Fix D)
X4 CHARGE TERMINÉE (collant): vehicleChargeCompleted, sourceMode='blocked'
│
├─ t5a: SOC < cible matin
│ X5a RECOVERY BATTERIE: takeover ON (EDF ON), ACM recharge → cible
│ │ t6: SOC ≥ cible
│ ▼
└─ t5b: SOC ≥ cible matin
X5b RECOVERY DONE (collant, Fix A): état 'night_recovery_done_idle'
│ takeover OFF définitif → G2 coupe EDF (turn_off_recovered)
│ ré-engagement UNIQUEMENT si SOC ≤ cible − 2 (bande anti-rebond)
│ t7: débranchement OU fin de fenêtre (07:24)
X6 CLÔTURE: clear session → retour X0 (profil jour)
G2 — HYSTÉRÉSIS EDF (seuils ON ≤ 20 % | OFF ≥ 30 % | min_on 25 | cooldown 15)
──────────────────────────────────────────────────────────────────────────────
Y0 EDF OFF (observe_only)
│ u1: override EVCS (takeover/force_grid/grid_forced) sans no-demand
│ u2: SOC ≤ seuil ON (bypass cooldown)
Y1 EDF ON + latch
│ v1: override disparu ET min_on écoulé ET SOC ≥ seuil OFF → turn_off_recovered
│ v2: force_grid + voiture pleine → coupure immédiate
│ v3: véhicule débranché + SOC ≥ seuil OFF → coupure immédiate
│ (turn_off_vehicle_unplugged, bypass min_on — Fix C 08/08/2026)
└─▶ retour Y0 (cooldown 15 min)
```
États surplus « fin de nuit » (aucun ne doit matcher les patterns no-demand
de G2 — full_or_paused / not_requesting_power / charge_complete / no_demand) :
- `night_session_done_idle` : charge finie, recovery batterie possible (EDF
maintenu si SOC < cible)
- `night_recovery_done_idle` : charge finie ET batterie à la cible → EDF
relâché définitivement (anti-rebond)
#### Incident de référence (nuit 07-08/08/2026)
SOC bloqué pile à la cible (35 %) = point dégénéré (engage `soc ≤ 35`,
release `soc > 35`) → rebond EDF ON/OFF période ~10 min + flapping ACM 2-4 s,
et EDF maintenu 25 min après débranchement. Corrigé par Fix A (flag collant +
bande 2 pts), Fix B (snapshot relit startSOC/startAt), Fix C (coupure au
débranchement), Fix D (statut charged = fin de charge immédiate).
--- ---
## 12. Configuration opencode.json ## 12. Configuration opencode.json

View File

@@ -170,3 +170,58 @@ Suite à la session de débogage du 17/07/2026:
- Mode sonde activé (probe_enabled: ON) - Mode sonde activé (probe_enabled: ON)
- Système fonctionnel avec delivery 99.9% - Système fonctionnel avec delivery 99.9%
- EDF OFF la journée, uniquement la nuit - EDF OFF la journée, uniquement la nuit
---
# Vision notes — Incident nuit 07→08/08/2026 (rebond EDF post-P8)
Source
9 captures : dashboard EVCS Master (08/08 11:31) + timeline Activité HA 01:00→07:00.
# OCR brut (faits clés)
- 01:25:03 Interrupteur hold_on_evcs_override | Grid=on | Latch=on | SOC=27% | AC=1021W |
Cause: night_grid_takeover_needed | PV 0W | EVCS inactif 0W | status_raw=6 |
signature es=battery_current_hard_stop
- 01:24:30→04:16 : "Victron EVCS Night Session Start At" RÉÉCRIT toutes les 30 s
(01:24:30, 01:25:00, 03:05:30, 03:59:30, 04:01:30...) + "Bloqué jusqu'à" glissant.
- 03:05 / 03:59 : hold_on_evcs_override maintenu, SOC 35%, seuils 20%/30%, PV 0W.
- 03:58:33→04:01:29 : "Victron mode (derived)" alterne Charging (AC-in) ↔ On grid
(passthru/import) toutes les 2-4 s (flapping ACM autour de SOC=cible).
- 04:16:03 turn_on_evcs_override : EDF re-forcé ON (Grid/Latch étaient OFF), SOC 35%,
cause night_grid_takeover_needed, es=night_session_done_idle. Latch hystérésis Activé.
- 04:18:03 hold_on_min_runtime (EDF maintenu ON, mini 25 min, 2 min écoulées),
status_raw=0, es=vehicle_not_connected ; Night Session Active + Takeover → Désactivés ;
VDF delivery=0 noise=0.003 score=0.002 noDemand=off ; victron_evcs_cloud_debt_kwh=1,1.
- 04:25:03 hysteresis_disabled | Grid=off | Latch=off | es=stopped_user (arrêt manuel).
- 04:26:03 "Dernier ON il y a 10 min | Dernier OFF il y a 11 min" → période de rebond ~10 min.
- Dashboard 11:31 : véhicule non détecté/débranché, consigne 20 A, durée charge 1h32m,
énergie session Inconnu ; Conseiller IA ombre : auto_solar_plus, cible 35%, accord,
modèle qwen3-vl, 35 accords / 4 divergences, kill-switch ON.
# État actuel déduit
- P7 (charge HC auto 01:24) a fonctionné ; VW pleine ~02:56 (1h32m).
- P8 a fini par émettre night_session_done_idle mais TARDIVEMENT, puis rebond EDF
ON/OFF (~10 min de période) : SOC bloqué pile à la cible matin (35%) =
gridTakeoverTrigger (soc<=35 → EDF ON) vs release (soc>35 → EDF OFF) sans bande.
- Débranchement VW (04:17) → takeover bien désactivé mais EDF resté ON (min_runtime 25)
→ arrêt manuel utilisateur 04:25.
# Causes code identifiées (src/nodes/)
1. WINDOWS_SOURCEV2.js:272-283+348-356 : gridTakeoverTriggerPct == batteryMorningTargetPct,
engage `soc <= 35`, release implicite `soc > 35`, recovery ACM `soc < 35` (strict)
→ point de fonctionnement dégénéré à SOC=cible → oscillation EDF + flapping ACM.
2. HA_SNAPSHOT_TEMPLATE.json.tpl : champs `evcs_night_session_start_soc` /
`evcs_night_session_start_at` ABSENTS → INPUT_PARSE ne relit jamais la capture →
nightSessionNeedsCapture=true à chaque cycle → réécriture toutes les 30 s (logs).
3. DECISION_ENGINEV2.js:322-337 : VDF armé uniquement session pleinement active ;
tout stop pour autre raison (ex battery_current_hard_stop) désarme le filtre →
détection "voiture pleine" retardée (~1h20 cette nuit).
4. Hystérésis grid (workflow 7iIlu7LLTW7TZA8o) : pas de coupure anticipée après
débranchement (min_on 25 min s'applique même si véhicule parti et SOC >= seuil OFF).
# Actions recommandées
- Fix A (prioritaire) : flag collant `night_recovery_done` (recovery terminée une fois
SOC >= cible) + bande d'hystérésis (engage cible-2, release cible) dans WINDOWS_SOURCEV2.
- Fix B : ajouter les 2 champs night_session_start_* au snapshot template.
- Fix C : coupure EDF anticipée si vehicle_not_connected + SOC >= gridSocOff (bypass min_on).
- Fix D (optionnel) : fiabiliser fin de charge (statut EVCS "charged", ou VDF armable en hold).
- +2 scénarios golden : SOC==cible avec vehicleChargeCompleted (pas d'oscillation), débranché
pendant hold EDF.

View File

@@ -383,6 +383,7 @@ const batteryAssistAllowed = !!nightPrev.batteryAssistAllowed;
const nightGridTakeoverNeeded = !!nightPrev.nightGridTakeoverNeeded; const nightGridTakeoverNeeded = !!nightPrev.nightGridTakeoverNeeded;
const nightTakeoverLatched = !!nightPrev.nightTakeoverLatched; const nightTakeoverLatched = !!nightPrev.nightTakeoverLatched;
const vehicleChargeCompleted = !!nightPrev.vehicleChargeCompleted; const vehicleChargeCompleted = !!nightPrev.vehicleChargeCompleted;
const nightRecoveryDone = !!nightPrev.nightRecoveryDone;
const nightPreferredSourceMode = nightPrev.nightPreferredSourceMode || 'blocked'; const nightPreferredSourceMode = nightPrev.nightPreferredSourceMode || 'blocked';
// ===================================================== // =====================================================
@@ -840,6 +841,7 @@ return [{
nightGridTakeoverNeeded, nightGridTakeoverNeeded,
nightTakeoverLatched, nightTakeoverLatched,
vehicleChargeCompleted, vehicleChargeCompleted,
nightRecoveryDone,
nightPreferredSourceMode, nightPreferredSourceMode,
}, },

View File

@@ -1064,11 +1064,18 @@ else {
// P8 : véhicule plein en session nuit → veille auto-entretenue. // P8 : véhicule plein en session nuit → veille auto-entretenue.
// Le state est relu au cycle suivant (prev_surplus_state) : il maintient // Le state est relu au cycle suivant (prev_surplus_state) : il maintient
// vehicleChargeCompleted jusqu'au débranchement ou à la fin de fenêtre. // vehicleChargeCompleted jusqu'au débranchement ou à la fin de fenêtre.
// Fix A (08/08/2026) : une fois la recovery batterie terminée (SOC ≥ cible),
// l'état devient 'night_recovery_done_idle' (collant) → takeover relâché
// définitivement, plus de rebond EDF autour de la cible.
else if (context.profile === 'night_hc' && nightEvcs?.vehicleChargeCompleted) { else if (context.profile === 'night_hc' && nightEvcs?.vehicleChargeCompleted) {
setOff({ setOff({
stateName: 'night_session_done_idle', stateName: nightEvcs?.nightRecoveryDone
? 'night_recovery_done_idle'
: 'night_session_done_idle',
decisionName: 'hold', decisionName: 'hold',
reasonText: 'Charge véhicule terminée : veille nuit, réseau relâché (recharge batterie si sous cible)', reasonText: nightEvcs?.nightRecoveryDone
? 'Charge véhicule terminée + batterie à la cible : veille nuit, réseau relâché'
: 'Charge véhicule terminée : veille nuit, réseau relâché (recharge batterie si sous cible)',
dynamic: false, dynamic: false,
mustStopFlag: false, mustStopFlag: false,
blockReason: 'none', blockReason: 'none',

View File

@@ -116,6 +116,8 @@
"allow_grid_hc_night": states("input_boolean.victron_evcs_allow_grid_hc_night"), "allow_grid_hc_night": states("input_boolean.victron_evcs_allow_grid_hc_night"),
"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"), "prev_surplus_state": states("input_text.victron_evcs_surplus_state"),
"evcs_night_session_start_soc": states("input_text.victron_evcs_night_session_start_soc"),
"evcs_night_session_start_at": states("input_text.victron_evcs_night_session_start_at"),
"allow_grid_hc_midday": states("input_boolean.victron_evcs_allow_grid_hc_midday"), "allow_grid_hc_midday": states("input_boolean.victron_evcs_allow_grid_hc_midday"),
"probe_enabled": states("input_boolean.victron_evcs_probe_enabled"), "probe_enabled": states("input_boolean.victron_evcs_probe_enabled"),

View File

@@ -317,11 +317,42 @@ const nightTakeoverPrev =
// → DECISION émet night_session_done_idle → le flag s'auto-entretient jusqu'au // → 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. // débranchement (state vehicle_not_connected) ou à la fin de la fenêtre nuit.
const prevSurplusState = (input.prev_surplus_state ?? '').toString().trim().toLowerCase(); const prevSurplusState = (input.prev_surplus_state ?? '').toString().trim().toLowerCase();
// Fix D (08/08/2026) : le statut natif EVCS "charged" (3) vaut aussi fin de
// charge, sans attendre le VehicleDemandFilter (désarmé si la session a été
// stoppée par une garde, ex battery_current_hard_stop → détection tardive
// observée la nuit du 07-08/08).
const evcsStatusCharged = Number.parseFloat(parsed.evcsStatusRaw) === 3;
const vehicleChargeCompleted = const vehicleChargeCompleted =
nightSessionActive && nightSessionActive &&
( (
prevSurplusState === 'vehicle_full_or_paused' || prevSurplusState === 'vehicle_full_or_paused' ||
prevSurplusState === 'night_session_done_idle' prevSurplusState === 'night_session_done_idle' ||
prevSurplusState === 'night_recovery_done_idle' ||
evcsStatusCharged
);
// Fix A (08/08/2026) : recovery batterie post-charge = UNE SEULE passe.
// Sans flag collant, SOC==cible est un point dégénéré (engage soc<=cible,
// release soc>cible, l'ACM recharge pile à la cible) → rebond EDF ON/OFF
// période ~10 min observé la nuit du 07-08/08. Une fois la cible atteinte,
// l'état 'night_recovery_done_idle' (auto-entretenu via prev_surplus_state)
// coupe le takeover. Ré-engagement uniquement sous cible − 2 pts (bande),
// hard floor et fenêtre/débranchement restent prioritaires.
// ⚠ 'night_recovery_done_idle' ne doit matcher AUCUN pattern no-demand de
// l'hystérésis (full_or_paused / not_requesting_power / charge_complete /
// no_demand) — même contrainte que 'night_session_done_idle'.
const NIGHT_RECOVERY_REARM_BAND_PCT = 2;
const nightRecoveryDonePrev = prevSurplusState === 'night_recovery_done_idle';
const nightRecoveryDone =
vehicleChargeCompleted &&
(
parsed.soc >= batteryMorningTargetPct ||
(
nightRecoveryDonePrev &&
parsed.soc > batteryMorningTargetPct - NIGHT_RECOVERY_REARM_BAND_PCT
)
); );
// P8 : correction du fix 04/08 (logique inversée : "|| vehicleChargeCompleted" // P8 : correction du fix 04/08 (logique inversée : "|| vehicleChargeCompleted"
@@ -350,9 +381,17 @@ const nightGridTakeoverNeeded =
( (
nightGridHcPreferred || nightGridHcPreferred ||
batteryBelowHardFloor || batteryBelowHardFloor ||
(
// Fix A : après fin de charge, le seul motif réseau restant est la
// recovery batterie, et elle ne se ré-engage pas une fois done.
vehicleChargeCompleted
? !nightRecoveryDone
: (
batteryAtOrBelowGridTakeoverTrigger || batteryAtOrBelowGridTakeoverTrigger ||
batteryRecoveryUrgent || batteryRecoveryUrgent ||
nightTakeoverLatched nightTakeoverLatched
)
)
); );
const nightPreferredSourceMode = const nightPreferredSourceMode =
@@ -679,6 +718,7 @@ return [{
nightTakeoverLatched, nightTakeoverLatched,
nightGridHcPreferred, nightGridHcPreferred,
vehicleChargeCompleted, vehicleChargeCompleted,
nightRecoveryDone,
nightPreferredSourceMode, nightPreferredSourceMode,
} }
} }

View File

@@ -138,7 +138,7 @@ export const scenarios = [
// ================= FIN DE CHARGE NUIT (P8) ================= // ================= FIN DE CHARGE NUIT (P8) =================
{ {
id: 'night-vw-pleine-transition', id: 'night-vw-pleine-transition',
label: 'Nuit HC 04:00, cycle après vehicle_full_or_paused → veille done_idle, takeover relâché', label: 'Nuit HC 04:00, cycle après vehicle_full_or_paused, SOC ≥ cible → recovery done, takeover relâché',
time: '2026-08-08T04:00:00+02:00', time: '2026-08-08T04:00:00+02:00',
overrides: { overrides: {
...VEHICLE_NIGHT, soc_percent: '60', allow_grid_hc_night: 'on', ...VEHICLE_NIGHT, soc_percent: '60', allow_grid_hc_night: 'on',
@@ -150,7 +150,7 @@ export const scenarios = [
expect: { expect: {
profile: 'night_hc', profile: 'night_hc',
sourceMode: 'blocked', sourceMode: 'blocked',
state: 'night_session_done_idle', state: 'night_recovery_done_idle',
decision: 'hold', decision: 'hold',
desiredCharging: false, desiredCharging: false,
currentA: 6, currentA: 6,
@@ -161,7 +161,7 @@ export const scenarios = [
}, },
{ {
id: 'night-vw-pleine-autoentretien', id: 'night-vw-pleine-autoentretien',
label: 'Nuit HC 05:00, veille done_idle auto-entretenue, EDF déjà coupé → reste en veille', label: 'Nuit HC 05:00, veille auto-entretenue SOC ≥ cible, EDF déjà coupé → reste en recovery done',
time: '2026-08-08T05:00:00+02:00', time: '2026-08-08T05:00:00+02:00',
overrides: { overrides: {
...VEHICLE_NIGHT, soc_percent: '55', allow_grid_hc_night: 'on', ...VEHICLE_NIGHT, soc_percent: '55', allow_grid_hc_night: 'on',
@@ -171,7 +171,7 @@ export const scenarios = [
expect: { expect: {
profile: 'night_hc', profile: 'night_hc',
sourceMode: 'blocked', sourceMode: 'blocked',
state: 'night_session_done_idle', state: 'night_recovery_done_idle',
decision: 'hold', decision: 'hold',
desiredCharging: false, desiredCharging: false,
currentA: 6, currentA: 6,
@@ -204,6 +204,99 @@ export const scenarios = [
}, },
}, },
// ===== FIX A/D 08/08/2026 : anti-rebond EDF post-charge (incident nuit 07-08/08) =====
{
id: 'night-recovery-atteinte-soc-cible',
label: 'Nuit HC 04:15, VW pleine, SOC 35 == cible (le cas du rebond) → recovery done, takeover coupé une fois pour toutes',
time: '2026-08-08T04:15:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '35', 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_recovery_done_idle',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'night-recovery-done-bande-collante',
label: 'Nuit HC 04:45, recovery done, SOC 34 dans la bande (cible−2 < 34 < 35) → PAS de ré-engagement (anti-rebond)',
time: '2026-08-08T04:45:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '34', allow_grid_hc_night: 'on',
evcs_charging_switch: 'off',
prev_surplus_state: 'night_recovery_done_idle',
},
expect: {
profile: 'night_hc',
sourceMode: 'blocked',
state: 'night_recovery_done_idle',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'night-recovery-rearm-sous-bande',
label: 'Nuit HC 05:30, recovery done mais SOC 32 ≤ cible−2 → ré-engagement takeover (recharge batterie)',
time: '2026-08-08T05:30:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '32', 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_recovery_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',
},
},
{
id: 'night-vw-statut-charged',
label: 'Nuit HC 03:00, statut EVCS brut 3 (charged), sans état VDF préalable → fin de charge immédiate (Fix D)',
time: '2026-08-08T03: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',
evcs_status_raw: '3',
prev_surplus_state: '',
},
expect: {
profile: 'night_hc',
sourceMode: 'blocked',
state: 'night_recovery_done_idle',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
// ================= JOUR SOLAIRE ================= // ================= JOUR SOLAIRE =================
{ {
id: 'morning-solaire-fort', id: 'morning-solaire-fort',

View File

@@ -0,0 +1,74 @@
#!/usr/bin/env node
// Patch 08/08/2026 — FIX C : coupure EDF immédiate au débranchement du véhicule.
// Incident nuit 07-08/08 : après débranchement, EDF restait ON 25 min
// (hold_on_min_runtime) → l'utilisateur a dû couper à la main.
// 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_vehicle_unplugged')) {
console.log('Déjà appliqué — rien à faire.');
process.exit(0);
}
const anchor = ` reason.push('Force grid + voiture pleine : coupure immediate du reseau');
forcedCut = true;
}
`;
const insertion = ` reason.push('Force grid + voiture pleine : coupure immediate du reseau');
forcedCut = true;
}
// ======================================================
// FIX C (08/08/2026): VEHICULE DEBRANCHE -> COUPURE IMMEDIATE
// Incident nuit 07-08/08 : apres debranchement du vehicule, EDF restait
// maintenu ON 25 min (hold_on_min_runtime) alors que plus rien ne le
// justifiait (SOC >= seuil OFF). Ici : coupure immediate, bypass min_on.
// ======================================================
if (
!forcedCut &&
latchIsOn &&
gridIsOn &&
!evcsGridOverride &&
evcsSurplusState === 'vehicle_not_connected' &&
soc >= gridSocOff
) {
desiredGrid = 'off';
desiredLatch = 'off';
state = 'turn_off_vehicle_unplugged';
reason.push('Vehicule debranche + SOC >= seuil OFF : coupure immediate du reseau (bypass min_on)');
forcedCut = true;
}
`;
if (!code.includes(anchor)) throw new Error('Ancre force_grid/forcedCut introuvable — code inattendu');
code = code.replace(anchor, insertion);
// Notification dédiée (facultative mais utile) : réutilise le bloc turn_off_recovered.
const notifAnchor = `} else if (state === 'turn_off_recovered') {`;
const notifInsertion = `} else if (state === 'turn_off_vehicle_unplugged') {
notifyTitle = '✅ EDF coupé (véhicule débranché)';
notifyMessage = \`Véhicule débranché et SOC \${round1(soc)}% >= \${gridSocOff}%. 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)`);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long