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:
69
HANDOFF.md
69
HANDOFF.md
@@ -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*
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -169,4 +169,59 @@ Suite à la session de débogage du 17/07/2026:
|
|||||||
- Configuration actuelle: soc_start_min=80%, night_target_soc=70%, battery_discharge_soft_a=0A
|
- Configuration actuelle: soc_start_min=80%, night_target_soc=70%, battery_discharge_soft_a=0A
|
||||||
- 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.
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|
||||||
|
|||||||
@@ -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 ||
|
||||||
batteryAtOrBelowGridTakeoverTrigger ||
|
(
|
||||||
batteryRecoveryUrgent ||
|
// Fix A : après fin de charge, le seul motif réseau restant est la
|
||||||
nightTakeoverLatched
|
// recovery batterie, et elle ne se ré-engage pas une fois done.
|
||||||
|
vehicleChargeCompleted
|
||||||
|
? !nightRecoveryDone
|
||||||
|
: (
|
||||||
|
batteryAtOrBelowGridTakeoverTrigger ||
|
||||||
|
batteryRecoveryUrgent ||
|
||||||
|
nightTakeoverLatched
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const nightPreferredSourceMode =
|
const nightPreferredSourceMode =
|
||||||
@@ -679,6 +718,7 @@ return [{
|
|||||||
nightTakeoverLatched,
|
nightTakeoverLatched,
|
||||||
nightGridHcPreferred,
|
nightGridHcPreferred,
|
||||||
vehicleChargeCompleted,
|
vehicleChargeCompleted,
|
||||||
|
nightRecoveryDone,
|
||||||
nightPreferredSourceMode,
|
nightPreferredSourceMode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
74
tools/patch-20260808-c-unplug-cut.mjs
Normal file
74
tools/patch-20260808-c-unplug-cut.mjs
Normal 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
459
workflows/victron-grid-hysteresis-v1.json
Normal file
459
workflows/victron-grid-hysteresis-v1.json
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user