Fix E: latch 'charge terminée' en session active + coupure recovery_done (incident rebond 08-09/08)
- DECISION: branche completed prioritaire dans la session active (avant VDF/source-lost) → état collant écrit au 1er cycle status=3, relais encore fermé - WINDOWS: garde puissance <=300W sur Fix D (anti-glitch) - Hystérésis: turn_off_recovery_done (bypass min_on, dry-run 5 cas) - golden 23→25 (session armée status 3, flap statut post-latch) - HANDOFF §18
This commit is contained in:
54
HANDOFF.md
54
HANDOFF.md
@@ -801,3 +801,57 @@ P7 a chargé la VW dès 01:24 ✓ (session 1h32m). Ensuite :
|
|||||||
|
|
||||||
---
|
---
|
||||||
*Document mis à jour le : 08/08/2026 (~14:15) — fixes A/B/C/D déployés, golden 23/23*
|
*Document mis à jour le : 08/08/2026 (~14:15) — fixes A/B/C/D déployés, golden 23/23*
|
||||||
|
|
||||||
|
## 18. Session du 09/08/2026 — Incident nuit 08-09/08 : le rebond est revenu par un AUTRE chemin (Fix E)
|
||||||
|
|
||||||
|
### Incident (captures HA, VW branchée, HC, VW pleine vers ~03:00)
|
||||||
|
Fix A/D du 08/08 ont bien fonctionné CÔTÉ WINDOWS (status 3 → completed →
|
||||||
|
takeover Désactivé, source blocked). MAIS l'état EVCS observé était
|
||||||
|
`hold_min_run_source_lost` / `stop_source_lost` — jamais `night_recovery_done_idle`.
|
||||||
|
Cycles ~30 min de 03:00 à 05:40 : EDF ON (2531 W réseau, SOC 51 % > cible 35)
|
||||||
|
→ arrêt urgence utilisateur 05:43 (EDF URGENCE OFF + stratégie stop) → débranchement.
|
||||||
|
|
||||||
|
### Root cause (ordre des branches DECISION)
|
||||||
|
1. Status 3 arrive PENDANT la session armée (relais fermé, chg=1/perm=1/l=1).
|
||||||
|
2. WINDOWS → sourceMode 'blocked' (Fix D) ; mais dans DECISION la branche
|
||||||
|
session ACTIVE traite 'blocked' comme « source perdue » :
|
||||||
|
`hold_min_run_source_lost` (25 min de session zombie !) puis `stop_source_lost`.
|
||||||
|
La branche P8 (état collant) est dans la chaîne NON-active → jamais atteinte.
|
||||||
|
3. Au stop, le relais s'ouvre → le statut EVCS RETOMBE (3 → 1/6, même piège que
|
||||||
|
le VDF documenté en P8) → completed=false → re-P7 → takeover ON 1 cycle →
|
||||||
|
`edf_general_ok_on` rallume EDF + restart → status 3 → goto 2. Rebond ~30 min.
|
||||||
|
4. L'hystérésis adoptait l'EDF ON du cycle de flap puis `hold_on_min_runtime`
|
||||||
|
25 min (ov=0/eg=0 ensuite, mais latch pris) → EDF ON quasi permanent.
|
||||||
|
|
||||||
|
### Fix E (déployé 09/08 ~06:25, golden 25/25)
|
||||||
|
- **DECISION** : nouvelle PREMIÈRE branche du bloc session-active (avant VDF) :
|
||||||
|
`night_hc && vehicleChargeCompleted && !force*` → setOff immédiat avec l'état
|
||||||
|
collant (`night_recovery_done_idle` si SOC ≥ cible, sinon
|
||||||
|
`night_session_done_idle`), decision `stop_vehicle_full_or_paused`.
|
||||||
|
→ l'état collant est écrit AU PREMIER cycle où status=3, relais encore fermé ;
|
||||||
|
les retombées de statut ensuite sont sans effet (prev state dans la liste).
|
||||||
|
- **WINDOWS** : garde puissance sur Fix D (`status==3 && evcsAcPowerW <= 300 W`)
|
||||||
|
pour ne jamais latcher sur un glitch en pleine charge.
|
||||||
|
- **Hystérésis** (patch tools/patch-20260809-e2-recovery-done-cut.mjs) :
|
||||||
|
`turn_off_recovery_done` = es=night_recovery_done_idle + pas d'override +
|
||||||
|
SOC ≥ seuil OFF → coupure EDF immédiate (bypass min_on 25). Sûr car l'état
|
||||||
|
est collant (le takeover ne peut pas revenir, sauf SOC ≤ cible−2 → override).
|
||||||
|
Dry-run 5 cas validé avant push.
|
||||||
|
|
||||||
|
### Golden : 23 → 25
|
||||||
|
- `night-charged-en-session-active` : status 3 en session ARMÉE → arrêt propre
|
||||||
|
+ latch immédiat (LE chemin de l'incident) ;
|
||||||
|
- `night-flap-statut-apres-latch` : relais ouvert, statut retombé à 1,
|
||||||
|
prev=night_recovery_done_idle → collant, aucun re-P7/EDF.
|
||||||
|
|
||||||
|
### Comportement attendu au REBRANCHEMENT d'une VW pleine en HC
|
||||||
|
1. Session nuit → P7 → EDF ON → tentative de charge (inévitable : le système
|
||||||
|
ne peut pas savoir que la VW est pleine sans fermer le relais) ;
|
||||||
|
2. VW refuse (0 W, status 3) → ≤ 1 min : `night_recovery_done_idle` collant,
|
||||||
|
takeover OFF, session stoppée proprement ;
|
||||||
|
3. Hystérésis : `turn_off_recovery_done` → EDF OFF ≤ 1 min (plus de min_on 25) ;
|
||||||
|
4. Plus RIEN jusqu'au débranchement / fin de fenêtre (flaps de statut ignorés).
|
||||||
|
Total attendu : EDF ON ~2-3 min UNE seule fois, puis silence.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Document mis à jour le : 09/08/2026 (~06:35) — Fix E déployé (EVCS + hystérésis), golden 25/25*
|
||||||
|
|||||||
@@ -629,6 +629,35 @@ else if (cloud.sessionObservedActive) {
|
|||||||
const effectiveHoldConditionsOk =
|
const effectiveHoldConditionsOk =
|
||||||
holdConditionsOk || manualLocalHoldOverride;
|
holdConditionsOk || manualLocalHoldOverride;
|
||||||
|
|
||||||
|
// =================================================
|
||||||
|
// Fix E (09/08/2026) : CHARGE TERMINÉE PENDANT LA SESSION ACTIVE.
|
||||||
|
// Nuit 08-09/08 : status EVCS 3 "charged" → WINDOWS passe la source à
|
||||||
|
// 'blocked' (Fix D), mais ICI les branches source-lost prenaient la main
|
||||||
|
// (hold_min_run_source_lost 25 min puis stop_source_lost), l'état collant
|
||||||
|
// n'était JAMAIS écrit ; à l'ouverture du relais le statut retombe (3→1),
|
||||||
|
// completed redevenait faux → re-P7 → EDF rallumé → rebond ~30 min.
|
||||||
|
// On latche l'état collant AU PREMIER cycle où la fin de charge est vue,
|
||||||
|
// relais encore fermé, et on termine la session proprement (pas de min ON).
|
||||||
|
// =================================================
|
||||||
|
if (
|
||||||
|
context.profile === 'night_hc' &&
|
||||||
|
nightEvcs?.vehicleChargeCompleted &&
|
||||||
|
!manualLocalChargeAllowed &&
|
||||||
|
!strategy.forceGridRequested
|
||||||
|
) {
|
||||||
|
setOff({
|
||||||
|
stateName: nightEvcs?.nightRecoveryDone
|
||||||
|
? 'night_recovery_done_idle'
|
||||||
|
: 'night_session_done_idle',
|
||||||
|
decisionName: 'stop_vehicle_full_or_paused',
|
||||||
|
reasonText: 'Charge véhicule terminée en session active : arrêt propre, veille nuit collante',
|
||||||
|
dynamic: false,
|
||||||
|
mustStopFlag: false,
|
||||||
|
blockReason: 'none',
|
||||||
|
blockedUntilStamp: now.stamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// =================================================
|
// =================================================
|
||||||
// VEHICLE FULL / PAUSED - NUMERIC FILTER DECISION
|
// VEHICLE FULL / PAUSED - NUMERIC FILTER DECISION
|
||||||
// IMPORTANT:
|
// IMPORTANT:
|
||||||
@@ -636,7 +665,7 @@ else if (cloud.sessionObservedActive) {
|
|||||||
// - uniquement si la source est réellement livrable
|
// - uniquement si la source est réellement livrable
|
||||||
// - sans utiliser le PV comme critère
|
// - sans utiliser le PV comme critère
|
||||||
// =================================================
|
// =================================================
|
||||||
if (
|
else if (
|
||||||
vehicleDemandFilter.evcsVehicleNoDemandSemantic &&
|
vehicleDemandFilter.evcsVehicleNoDemandSemantic &&
|
||||||
vehicleDemandFilter.sourceDeliverableForNoDemand
|
vehicleDemandFilter.sourceDeliverableForNoDemand
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -322,7 +322,13 @@ const prevSurplusState = (input.prev_surplus_state ?? '').toString().trim().toLo
|
|||||||
// charge, sans attendre le VehicleDemandFilter (désarmé si la session a été
|
// 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
|
// stoppée par une garde, ex battery_current_hard_stop → détection tardive
|
||||||
// observée la nuit du 07-08/08).
|
// observée la nuit du 07-08/08).
|
||||||
const evcsStatusCharged = Number.parseFloat(parsed.evcsStatusRaw) === 3;
|
// Fix E (09/08/2026) : garde puissance ≤ 300 W pour ne jamais latcher "fini"
|
||||||
|
// sur un glitch de statut pendant une vraie charge. NB : le statut retombe
|
||||||
|
// (3→1/6) dès l'ouverture du relais → la persistance est assurée par l'état
|
||||||
|
// collant écrit par DECISION pendant la session (voir Fix E DECISION).
|
||||||
|
const evcsStatusCharged =
|
||||||
|
Number.parseFloat(parsed.evcsStatusRaw) === 3 &&
|
||||||
|
Math.max(0, parsed.evcsAcPowerW || 0) <= 300;
|
||||||
|
|
||||||
const vehicleChargeCompleted =
|
const vehicleChargeCompleted =
|
||||||
nightSessionActive &&
|
nightSessionActive &&
|
||||||
|
|||||||
@@ -297,6 +297,60 @@ export const scenarios = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ===== FIX E 09/08/2026 : latch "charge terminée" PENDANT la session active =====
|
||||||
|
// Incident nuit 08-09/08 : status 3 en session armée → source-lost zombie 25 min,
|
||||||
|
// état collant jamais écrit, statut retombe à l'ouverture du relais → rebond EDF.
|
||||||
|
{
|
||||||
|
id: 'night-charged-en-session-active',
|
||||||
|
label: 'Nuit HC 04:30, session ARMÉE (relais fermé, 0 W), statut 3 → arrêt propre + latch collant immédiat',
|
||||||
|
time: '2026-08-09T04:30:00+02:00',
|
||||||
|
overrides: {
|
||||||
|
...VEHICLE_NIGHT, soc_percent: '51', allow_grid_hc_night: 'on',
|
||||||
|
grid_state: 'on', victron_acin1_available: '1',
|
||||||
|
battery_current_a: '2', night_takeover_prev: 'on',
|
||||||
|
evcs_charging_switch: 'on',
|
||||||
|
charge_permission_now: 'on',
|
||||||
|
control_latch: 'on',
|
||||||
|
evcs_current_a: '0',
|
||||||
|
evcs_ac_power_w: '0',
|
||||||
|
evcs_status_raw: '3',
|
||||||
|
prev_surplus_state: 'charging_hold',
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: 'night_hc',
|
||||||
|
sourceMode: 'blocked',
|
||||||
|
state: 'night_recovery_done_idle',
|
||||||
|
decision: 'stop_vehicle_full_or_paused',
|
||||||
|
desiredCharging: false,
|
||||||
|
currentA: 6,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: 'turn_off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'night-flap-statut-apres-latch',
|
||||||
|
label: 'Nuit HC 05:00, relais ouvert, statut retombé à 1, prev=night_recovery_done_idle → collant, AUCUN re-P7/EDF',
|
||||||
|
time: '2026-08-09T05:00:00+02:00',
|
||||||
|
overrides: {
|
||||||
|
...VEHICLE_NIGHT, soc_percent: '51', allow_grid_hc_night: 'on',
|
||||||
|
evcs_charging_switch: 'off',
|
||||||
|
evcs_status_raw: '1',
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// ================= JOUR SOLAIRE =================
|
// ================= JOUR SOLAIRE =================
|
||||||
{
|
{
|
||||||
id: 'morning-solaire-fort',
|
id: 'morning-solaire-fort',
|
||||||
|
|||||||
77
tools/patch-20260809-e2-recovery-done-cut.mjs
Normal file
77
tools/patch-20260809-e2-recovery-done-cut.mjs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Patch 09/08/2026 — FIX E2 (relais) : coupure EDF immédiate quand l'état
|
||||||
|
// surplus est 'night_recovery_done_idle' (collant : le takeover ne peut plus
|
||||||
|
// revenir) et qu'aucun override n'est actif. Évite les 25 min de
|
||||||
|
// hold_on_min_runtime après la tentative de charge d'un véhicule déjà plein.
|
||||||
|
// 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_recovery_done')) {
|
||||||
|
console.log('Déjà appliqué — rien à faire.');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const anchor = ` state = 'turn_off_vehicle_unplugged';
|
||||||
|
reason.push('Vehicule debranche + SOC >= seuil OFF : coupure immediate du reseau (bypass min_on)');
|
||||||
|
forcedCut = true;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const insertion = ` state = 'turn_off_vehicle_unplugged';
|
||||||
|
reason.push('Vehicule debranche + SOC >= seuil OFF : coupure immediate du reseau (bypass min_on)');
|
||||||
|
forcedCut = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// FIX E2 (09/08/2026): RECOVERY DONE -> COUPURE IMMEDIATE
|
||||||
|
// 'night_recovery_done_idle' est un etat COLLANT du controleur EVCS :
|
||||||
|
// charge terminee ET batterie a la cible, le takeover ne reviendra pas
|
||||||
|
// (sauf SOC <= cible-2, qui repasserait par l'override). Attendre le
|
||||||
|
// min_on 25 min ne protege ici de rien : coupure immediate.
|
||||||
|
// ======================================================
|
||||||
|
if (
|
||||||
|
!forcedCut &&
|
||||||
|
latchIsOn &&
|
||||||
|
gridIsOn &&
|
||||||
|
!evcsGridOverride &&
|
||||||
|
evcsSurplusState === 'night_recovery_done_idle' &&
|
||||||
|
soc >= gridSocOff
|
||||||
|
) {
|
||||||
|
desiredGrid = 'off';
|
||||||
|
desiredLatch = 'off';
|
||||||
|
state = 'turn_off_recovery_done';
|
||||||
|
reason.push('Charge terminee + batterie a la cible : coupure immediate du reseau (bypass min_on)');
|
||||||
|
forcedCut = true;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (!code.includes(anchor)) throw new Error('Ancre turn_off_vehicle_unplugged introuvable — appliquer d abord patch-20260808-c-unplug-cut.mjs');
|
||||||
|
code = code.replace(anchor, insertion);
|
||||||
|
|
||||||
|
const notifAnchor = `} else if (state === 'turn_off_recovered') {`;
|
||||||
|
const notifInsertion = `} else if (state === 'turn_off_recovery_done') {
|
||||||
|
notifyTitle = '✅ EDF coupé (charge terminée)';
|
||||||
|
notifyMessage = \`Charge VE terminée et batterie à la cible (SOC \${round1(soc)}%). 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
Reference in New Issue
Block a user