Un depassement souple de la batterie doit durer pour compter

La table des bandes SOC declarait depuis toujours un courant souple tolere
pendant des minutes et un courant dur tolere quelques secondes. Les deux
tolerances etaient calculees, exportees dans le debug, et lues par aucune
decision : le controleur comparait un echantillon instantane a un plancher,
toutes les 30 s.

A SOC 94 la batterie est en float et son courant oscille entre +1,8 et -2,6 A
d'un echantillon a l'autre. Le test devenait un tirage a pile ou face et la
charge du vehicule s'arretait sur du bruit : deux fois le 01/09, quatre a six
fois par jour la semaine precedente. Chaque arret coutait environ 0,45 kWh,
la batterie etant pleine et le vehicule le seul preneur.

Le correctif est purement temporel : aucun seuil n'est deplace, aucune
constante ajoutee. La tolerance de la bande est branchee sur les trois
conditions de MAINTIEN, via un helper de persistance qui porte le debut du
depassement en cours. Le plancher de demarrage et le garde dur restent
instantanes : la tolerance est du confort, jamais du risque.

Golden 56 -> 61. Preuve par mutation dans les deux sens : tolerance supprimee
-> 2 echecs cibles, tolerance infinie -> 3 echecs cibles, l'arret dur restant
vert dans les deux cas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
This commit is contained in:
Tallagrand Thierry
2026-09-01 20:10:28 +02:00
parent a7ebaabb8b
commit 10987a04d8
9 changed files with 428 additions and 11 deletions

View File

@@ -2352,3 +2352,136 @@ pendant toute la session), SOC stable à 49 %.
propriétaire), en plus de `~/.config/opencode/opencode.json`. Le repli du chien
de garde sur `.env` est vérifié — utile pour tout déploiement hors poste de
travail.
## 35. Session du 01/09/2026 — La table des bandes SOC déclarait un temps que personne ne lisait
### Bilan de la nuit 31/08→01/09 (contexte de la découverte)
Deux sessions réseau : 01:24→03:32 (charge HC, **6,51 kWh importés**, dont
**5,14 kWh dans la VW**, batterie tenue entre −2 et +2 A, SOC plat à 49 %) et
07:12→09:51 (batterie tombée à 20 %, reprise par l'hystérésis, **0,70 kWh**).
À l'ouverture du contacteur de 09:51, en plein jour : PV 2,7 kW avant, 2,7 kW
après, import résiduel 40 W — **aucune éviction du solaire**, la correction de
consigne du 31/08 tient. Journée : 27,85 kWh produits, 6,81 kWh dans la voiture
en solaire, 0,39 kWh de réseau.
**Le point faible de cette nuit n'est pas un bug, c'est la définition de la
cible.** À 03:31 le SOC (49 %) dépassait la cible du matin (35 %) → recovery
done, EDF relâché. Trois heures quarante plus tard la batterie était à 20 % et
l'hystérésis a dû rallumer. Pour traverser jusqu'au relais solaire (~09:00) il
aurait fallu partir de **~64 %**. La cible est un niveau à un instant ; ce qu'il
faut est une énergie à couvrir jusqu'au croisement PV/conso. C'est le chantier
du bilan énergétique, pas un seuil de plus.
### Le défaut : un critère physique évalué sur du bruit
Deux arrêts `stop_conditions_lost` le 01/09, à **14:26:00** et **14:56:30**,
véhicule en charge à 20 A sous 6,6 kW de PV.
La règle de maintien exige `courant batterie ≥ plancher`, plancher effectif
**0 A** (`input_number.victron_evcs_battery_discharge_soft_a`, curseur borné
à [−50, 0]). Or à SOC 94 la batterie est en float. Mesure brute au pas de 2 s
dans la minute précédant l'arrêt :
```
14:25:34 +1,8 14:25:44 0,0 14:25:54 −0,4
14:25:38 −1,0 14:25:46 −0,4 14:25:56 −1,6
14:25:40 −2,6 14:25:50 +1,0 14:25:58 −0,7
```
Ce n'est pas une décharge, **c'est le bruit de régulation d'une batterie qui ne
fait rien**. Le contrôleur en prélève un échantillon toutes les 30 s et le
compare à zéro : pile ou face. Les états `hold_min_run` qui alternent avec
`charging_hold` toutes les 1 à 2 minutes dans le journal SONT ces tirages
perdants — sauvés uniquement parce que le `min_run` (29 min) n'était pas
écoulé. Quand un échantillon négatif tombe après le `min_run`, la charge
s'arrête. Le second arrêt avait une cause réelle mais fugace : pointe maison à
**8 016 W** pendant quelques secondes, batterie à −42 A.
**Coût mesuré :** à l'arrêt la PV s'effondre de 6 633 à **2 554 W** en 30 s —
batterie pleine, le véhicule était le seul preneur — et la reprise repart à 6 A
pour mettre **21 minutes** à revenir à 20 A. ≈ **0,45 kWh de production refusée
par arrêt**. Fréquence : 1 le 26/08, 4 le 27, 6 le 28, 4 le 29, 4 le 30.
### Ce que le système savait déjà, et que rien ne lisait
`INPUT_PARSE` construit depuis toujours une table de politique batterie par
bande de SOC, avec **deux régimes explicitement temporels** :
| bande | souple | tolérance souple | dur | tolérance dure |
|---|---:|---:|---:|---:|
| > 90 | −50 A | **12 min** | −100 A | 20 s |
| 80-90 | −40 A | 10 min | −80 A | 20 s |
| 60-80 | −20 A | 6 min | −50 A | 15 s |
| 40-60 | −12 A | 4 min | −30 A | 10 s |
| 25-40 | −5 A | 2 min | −15 A | 5 s |
| < 25 | −3 A | 1 min | −8 A | 2 s |
`batterySoftToleranceMinutes` et `batteryHardToleranceSeconds` étaient calculés
(BATTERY_CLOUD_LOCKS l.185-186), exportés dans le debug, et **branchés sur
aucune décision**. Toute la dimension temporelle de la politique batterie était
morte. Troisième mécanisme dans ce cas — `houseSpikeDetected` n'allonge que la
tolérance *nuage*, jamais la garde batterie, et son entrée
`house_ac_power_prev_w` lit **le même capteur** que `house_ac_power_w` dans le
gabarit de snapshot, donc il vaut structurellement `false` (non corrigé ici,
la tolérance rend ce chemin redondant — à traiter séparément).
### Correctif (déployé 01/09 20:08, golden 61/61)
**Purement temporel : aucun seuil n'est déplacé, aucune constante ajoutée.**
On applique la table telle qu'elle est écrite — un dépassement souple doit
DURER pour compter.
- Nouveau helper de persistance `input_text.victron_evcs_battery_soft_breach_at`
(paquet `victron_evcs_battery_dynamic.yaml`, sauvegarde dans `packages/SAVE/`,
créé par `input_text.reload` — **aucun redémarrage HA**). Vide = pas de
dépassement. Écrit par le contrôleur, uniquement sur changement réel : la date
de début doit rester figée, sinon le compteur ne monte jamais.
- `BATTERY_CLOUD_LOCKS` : `batterySoftBreachNow` / `...StartedMs` /
`...Minutes`, et `batterySoftDischargeHoldOk` = pas de dépassement, ou
dépassement plus court que la tolérance de sa bande.
- Branché sur les **trois conditions de MAINTIEN** qui utilisaient le test
instantané : `rawSolarHoldOk`, `batteryAssistSolarHoldOk`, `probeHoldGuardOk`.
(`gridHoldOk` et `batteryAssistNightHoldOk` utilisaient déjà le seuil DUR.)
**Ce qui n'est PAS touché, et pourquoi.** Le plancher de **démarrage** reste
instantané : ne pas lancer une charge sur une batterie qui se décharge déjà,
c'est la frontière « délestage et non charge prioritaire ». Le garde **dur**
reste instantané : `hardToleranceSec` n'est volontairement pas branché, cela ne
pourrait que ralentir une sécurité. La tolérance est du confort, jamais du
risque.
**Pourquoi ce filtre discrimine.** Le bruit change de signe en quelques
secondes, donc le compteur se remet à zéro de lui-même et n'atteint jamais la
tolérance. Une vraie décharge reste négative à chaque échantillon et finit par
la dépasser. C'est *parce que* le signal est du bruit que le filtre marche.
### Golden 56 → 61
`batt-float-bruit-ne-coupe-pas` (relevé du 14:26 : SOC 94, −1 A → `charging_hold`
à 20 A) · `batt-decharge-soutenue-coupe` (CONTREFACTUEL, mêmes mesures mais
dépassement de 20 min > 12 → `stop_conditions_lost`) · `batt-pointe-maison-ne-coupe-pas`
(relevé du 14:56, −42 A → réduction graduée au plafond système 18 A, **pas** un
arrêt) · `batt-decharge-dure-arret-immediat` (−120 A < seuil dur −100 →
`battery_current_hard_stop`, aucune tolérance) · `batt-plancher-demarrage-reste-instantane`
(véhicule à l'arrêt, −1 A → `idle_wait_conditions`).
Les trois scénarios de la fenêtre d'établissement portent désormais **le même
dépassement en cours** (10 min > tolérance 4 min de leur bande) : sans cela, la
tolérance les rendait tous identiques et la paire ne prouvait plus rien.
Preuve par mutation, les deux sens séparément :
- tolérance supprimée (comportement d'avant) → 2 échecs ciblés, et
`etablissement-maintien-charge` reste vert (c'est bien la fenêtre qui le
protège, pas la tolérance) ;
- tolérance infinie → 3 échecs ciblés, et l'arrêt dur reste vert (la sécurité ne
dépend pas de la tolérance) ;
- restauration → 61/61.
### À surveiller
1. Journal : plus aucun `stop_conditions_lost` en pleine charge solaire à
batterie pleine ; la trace `Batt souple X/12min` n'apparaît que pendant un
dépassement réel.
2. `input_text.victron_evcs_battery_soft_breach_at` : vide la plupart du temps,
écrit une fois au début d'un dépassement, effacé à la fin. S'il reste rempli
en permanence, c'est un vrai déficit à instruire, pas un bug du filtre.
3. Debug : `batt_soft_breach_now`, `batt_soft_breach_minutes`,
`batt_soft_discharge_hold_ok`.
---
*Document mis à jour le : 01/09/2026 (~20:15) — tolérance temporelle de la politique batterie branchée, golden 61/61*

View File

@@ -717,6 +717,39 @@ charge EVCS), lisser le SOC pour les décisions de confort en gardant
l'instantané pour la sécurité, et exploiter les messages d'état réels du
Victron et de la borne plutôt que de s'auto-limiter à l'aveugle sur un proxy.
#### C. Arrêt de la charge sur le bruit de la mesure batterie (01/09/2026)
Troisième membre de la même famille, découvert le 01/09 : la règle de maintien
comparait le **courant batterie instantané** à un plancher de 0 A, toutes les
30 s. À SOC 94 % la batterie est en float et son courant oscille entre +1,8 et
−2,6 A d'un échantillon à l'autre — le test devenait un tirage à pile ou face.
Deux arrêts ce jour-là (14:26 et 14:56), 1 à 6 par jour la semaine précédente.
Chaque arrêt coûtait ~0,45 kWh : batterie pleine, le véhicule était le **seul
preneur**, donc la PV s'effondrait de 6,6 à 2,5 kW en 30 s et la reprise mettait
21 min à remonter de 6 A à 20 A.
La table de politique batterie par bande de SOC (`INPUT_PARSE`) déclarait
pourtant déjà, pour chaque bande, un courant **souple** toléré pendant des
**minutes** (12 min au-dessus de 90 %) et un courant **dur** toléré quelques
**secondes**. Les deux tolérances étaient calculées, exportées dans le debug, et
lues par **aucune décision** : toute la dimension temporelle de la politique
était morte.
Correctif : brancher la tolérance souple sur les trois conditions de maintien,
via un helper de persistance
`input_text.victron_evcs_battery_soft_breach_at` qui porte le début du
dépassement en cours. **Aucun seuil déplacé, aucune constante ajoutée.** Le
plancher de démarrage et le garde dur restent instantanés — la tolérance est du
confort, jamais du risque. Le filtre discrimine parce que le bruit change de
signe en quelques secondes (compteur remis à zéro) alors qu'une vraie décharge
reste négative à chaque échantillon.
Golden 56 → 61, dont le contrefactuel `batt-decharge-soutenue-coupe` qui prouve
que la garde mord toujours après sa tolérance. Détail : HANDOFF §35.
---
---
## 12. Configuration opencode.json

View File

@@ -453,6 +453,58 @@ const profileSpecificDischargeFloorA =
const profileBatteryDischargeFloorA =
Math.max(profileSpecificDischargeFloorA, batteryDischargeSoftDynamicA);
// =====================================================
// DEPASSEMENT SOUPLE DU PLANCHER DE DECHARGE (01/09/2026)
// =====================================================
// La table des bandes SOC (INPUT_PARSE) declare depuis toujours DEUX regimes :
// un courant SOUPLE tolere pendant des MINUTES (softToleranceMin : 12 min a
// SOC > 90, 1 min sous 25) et un courant DUR tolere quelques SECONDES. Seuls
// les seuils de courant etaient lus ; la dimension temporelle etait calculee,
// exportee dans le debug, et branchee sur AUCUNE decision. Le controleur
// comparait donc un echantillon instantane, toutes les 30 s, a un plancher.
//
// Incident du 01/09 (14:26 et 14:56, SOC 94, batterie en float) : le courant
// batterie oscillait entre +1,8 et -2,6 A d'un echantillon a l'autre, soit le
// bruit de regulation d'une batterie qui ne fait rien. Le test devenait un
// tirage a pile ou face et la charge du vehicule s'arretait dessus. Chaque
// arret coutait ~0,45 kWh de production refusee : batterie pleine, le vehicule
// etait le SEUL preneur, donc la PV s'effondrait de 6,6 a 2,5 kW en 30 s, et
// la reprise mettait 21 min a remonter de 6 A a 20 A. 1 a 6 arrets par jour
// sur la semaine du 26 au 31/08.
//
// Le correctif est PUREMENT TEMPOREL : aucun seuil n'est deplace, aucune
// constante n'est ajoutee. On applique la table telle qu'elle est ecrite :
// un depassement souple doit DURER pour compter. Comme le bruit change de
// signe en quelques secondes, le compteur se remet a zero de lui-meme ; une
// vraie decharge, elle, reste negative a chaque echantillon et finit par
// depasser la tolerance de sa bande.
//
// CE QUI N'EST PAS TOUCHE : le plancher de DEMARRAGE reste instantane (ne pas
// lancer une charge sur une batterie qui se decharge deja, c'est la frontiere
// "delestage et non charge prioritaire"), et le garde DUR reste instantane —
// hardToleranceSec n'est volontairement PAS branche, cela ne pourrait que
// ralentir une securite, et c'est le risque qui prime.
const batterySoftBreachNow =
sessionObservedActive &&
parsed.batteryCurrentA < profileBatteryDischargeFloorA;
const batterySoftBreachStartedMs = batterySoftBreachNow
? (history.batterySoftBreachMsPrev !== null
? history.batterySoftBreachMsPrev
: nowMs)
: null;
const batterySoftBreachMinutes =
batterySoftBreachStartedMs !== null
? Math.max(0, (nowMs - batterySoftBreachStartedMs) / 60000)
: 0;
// Vrai tant que le depassement n'a pas dure plus que la tolerance de sa bande.
// Une tolerance a 0 minute redonne exactement le comportement instantane.
const batterySoftDischargeHoldOk =
!batterySoftBreachNow ||
batterySoftBreachMinutes <= batterySoftToleranceMinutes;
// =====================================================
// GRID RELAX IN force_charge_now
// =====================================================
@@ -557,7 +609,7 @@ const batteryAssistSolarHoldOk =
commonSafetyHoldOk &&
!isGridSource &&
parsed.soc >= Math.max(profileSocHoldFloor, batteryAssistSolarHoldSocMin) &&
parsed.batteryCurrentA >= profileBatteryDischargeFloorA &&
batterySoftDischargeHoldOk &&
cloudToleranceOk;
// =====================================================
@@ -591,7 +643,7 @@ const rawSolarHoldOk =
parsed.soc >= effectiveSocHoldFloor &&
(
gridSettling ||
parsed.batteryCurrentA >= profileBatteryDischargeFloorA
batterySoftDischargeHoldOk
) &&
(
gridSettling ||
@@ -655,7 +707,7 @@ const probeStartOk =
const probeHoldGuardOk =
probeModeActive &&
commonSafetyHoldOk &&
parsed.batteryCurrentA >= profileBatteryDischargeFloorA;
batterySoftDischargeHoldOk;
const probeWindowActive =
probeModeActive &&
@@ -824,6 +876,14 @@ return [{
batteryHardToleranceSeconds,
batteryRecoveryMinutes,
// Depassement souple en cours (01/09/2026). startedMs = null quand il
// n'y a pas de depassement : c'est ce que HA_SERVICE_CALLS_OUTPUT lit
// pour ecrire ou effacer le helper de persistance.
batterySoftBreachNow,
batterySoftBreachStartedMs,
batterySoftBreachMinutes,
batterySoftDischargeHoldOk,
gridBatterySoftDynamicA,
gridBatteryHardDynamicA,
gridForceChargeRelaxSoftA,

View File

@@ -67,6 +67,12 @@ pushAction(
!!decision.set_cloud_debt_started_at
);
pushAction(
'helper_battery_soft_breach_at',
calls.helper_battery_soft_breach_at,
!!calls.helper_battery_soft_breach_at?.execute
);
pushAction(
'helper_block_reason',
calls.helper_block_reason,

View File

@@ -387,6 +387,13 @@ if (derived.gridSettling) {
if (gridSessionActive) {
reason.push(`Consigne reseau=${gridSetpointTargetW}W`);
}
// Visible dans le logbook uniquement pendant un depassement souple : c'est le
// seul moment ou l'information sert, et la raison est tronquee a 255 caracteres.
if (batteryPolicy.batterySoftBreachNow) {
reason.push(
`Batt souple ${round1(batteryPolicy.batterySoftBreachMinutes)}/${round1(batteryPolicy.batterySoftToleranceMinutes)}min`
);
}
reason.push(`Cap=${derived.physicalCapA}A`);
reason.push(`Ref=${round1(derived.currentReferenceA)}A`);
reason.push(`Target=${round1(desiredCurrentA)}A`);
@@ -498,6 +505,19 @@ const cloudDebtStartedAtToWrite =
? formatParisStampFromMs(cloud.cloudDebtStartedMs)
: now.stamp;
// DEPASSEMENT SOUPLE BATTERIE (01/09/2026) : chaine vide quand il n'y a pas
// de depassement en cours. Le helper n'est reecrit que sur changement reel
// (debut ou fin), pas a chaque cycle : la valeur du debut doit RESTER figee
// pendant toute la duree du depassement, sinon le compteur ne monte jamais.
const batterySoftBreachAtToWrite =
batteryPolicy.batterySoftBreachStartedMs !== null &&
batteryPolicy.batterySoftBreachStartedMs !== undefined
? formatParisStampFromMs(batteryPolicy.batterySoftBreachStartedMs)
: '';
const setBatterySoftBreachAt =
batterySoftBreachAtToWrite !== (history.batterySoftBreachAt || '');
const probeStartedAtToWrite = now.stamp;
const lastStartAtToWrite = now.stamp;
const lastStopAtToWrite = now.stamp;
@@ -587,6 +607,10 @@ return [{
house_spike_detected: batteryPolicy.houseSpikeDetected,
solar_potential_context: batteryPolicy.solarPotentialContext,
batt_soft_breach_now: !!batteryPolicy.batterySoftBreachNow,
batt_soft_breach_minutes: round1(batteryPolicy.batterySoftBreachMinutes || 0),
batt_soft_discharge_hold_ok: batteryPolicy.batterySoftDischargeHoldOk !== false,
evcs_priority_active: evcsPriorityActive,
energy_priority_mode: energyPriorityMode,
energy_priority_reason: energyPriorityReason,
@@ -797,6 +821,16 @@ return [{
}
},
helper_battery_soft_breach_at: {
execute: setBatterySoftBreachAt,
domain: 'input_text',
service: 'set_value',
data: {
entity_id: 'input_text.victron_evcs_battery_soft_breach_at',
value: batterySoftBreachAtToWrite
}
},
helper_last_current_change_at: {
execute: setLastCurrentChangeAt,
domain: 'input_datetime',

View File

@@ -84,6 +84,7 @@
"cloud_debt_max_kwh": states("input_number.victron_evcs_cloud_debt_max_kwh"),
"cloud_tolerance_minutes": states("input_number.victron_evcs_cloud_tolerance_minutes"),
"cloud_debt_started_at": states("input_datetime.victron_evcs_cloud_debt_started_at"),
"battery_soft_breach_at": states("input_text.victron_evcs_battery_soft_breach_at"),
"operating_mode_helper": states("input_text.victron_evcs_operating_mode"),
"departure_time": states("input_datetime.victron_evcs_departure_time"),

View File

@@ -538,6 +538,12 @@ const history = {
lastStopMs: parseParisStampToMs(cleanText(input.last_stop_at)),
lastCurrentChangeMs: parseParisStampToMs(cleanText(input.last_current_change_at)),
cloudDebtStartedMsPrev: parseParisStampToMs(cleanText(input.cloud_debt_started_at)),
// DEPASSEMENT SOUPLE BATTERIE (01/09/2026) : horodatage du debut du
// depassement en cours, ecrit par HA_SERVICE_CALLS_OUTPUT. Vide/illisible
// => null => aucun depassement en cours, comportement de repli sur.
batterySoftBreachAt: cleanText(input.battery_soft_breach_at),
batterySoftBreachMsPrev: parseParisStampToMs(cleanText(input.battery_soft_breach_at)),
evcsPriorityManualUntilMs: parseParisStampToMs(cleanText(input.evcs_priority_manual_until)),
// =====================================================

View File

@@ -909,6 +909,147 @@ export const scenarios = [
},
},
// ===== 01/09/2026 : le depassement souple de la batterie doit DURER =====
// Incident du 01/09 (14:26 et 14:56) : SOC 94, batterie en float, courant
// oscillant entre +1,8 et -2,6 A d'un echantillon a l'autre. Le controleur
// comparait un echantillon instantane a un plancher de 0 A toutes les 30 s :
// pile ou face. Chaque arret coutait ~0,45 kWh de production refusee, la
// voiture etant le seul preneur d'une batterie pleine.
// La table des bandes SOC declarait deja une tolerance de 12 min a SOC > 90 ;
// elle n'etait branchee nulle part. Aucun seuil n'est deplace ici.
{
id: 'batt-float-bruit-ne-coupe-pas',
label: "Relevé du 01/09 14:26 : SOC 94, batterie en float à -1 A, charge à 20 A → le bruit de régulation NE DOIT PAS arrêter la charge",
time: AFTERNOON,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
evcs_charging_switch: 'on', control_latch: 'on', charge_permission_now: 'on',
last_start_at: '2026-08-08 14:00:00',
soc_percent: '94',
evcs_current_a: '20', evcs_set_current_a: '20', evcs_command_current_a: '20',
evcs_ac_power_w: '3600',
pv_power_w: '6600', pv_power_10m_avg_w: '6000', pv_trend_w: '0',
house_ac_power_w: '6000', vebus_ac_out_l1_power_w: '6000',
battery_current_a: '-1',
},
expect: {
profile: 'outside_window',
sourceMode: 'solar_only',
state: 'charging_hold',
decision: 'hold',
desiredCharging: true,
currentA: 20,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'batt-decharge-soutenue-coupe',
label: "CONTREFACTUEL : mêmes mesures, mais le dépassement dure depuis 20 min > tolérance 12 min de la bande gt90 → la garde reprend la main et arrête",
time: AFTERNOON,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
evcs_charging_switch: 'on', control_latch: 'on', charge_permission_now: 'on',
last_start_at: '2026-08-08 14:00:00',
soc_percent: '94',
evcs_current_a: '20', evcs_set_current_a: '20', evcs_command_current_a: '20',
evcs_ac_power_w: '3600',
pv_power_w: '6600', pv_power_10m_avg_w: '6000', pv_trend_w: '0',
house_ac_power_w: '6000', vebus_ac_out_l1_power_w: '6000',
battery_current_a: '-1',
battery_soft_breach_at: '2026-08-08 15:10:00',
},
expect: {
profile: 'outside_window',
sourceMode: 'solar_only',
state: 'stop_conditions_lost',
decision: 'stop',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'batt-pointe-maison-ne-coupe-pas',
label: "Relevé du 01/09 14:56 : pointe maison à 8 kW, batterie à -42 A → réduction graduée au plafond système (18 A), PAS un arrêt",
time: AFTERNOON,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
evcs_charging_switch: 'on', control_latch: 'on', charge_permission_now: 'on',
last_start_at: '2026-08-08 14:00:00',
soc_percent: '94',
evcs_current_a: '20', evcs_set_current_a: '20', evcs_command_current_a: '20',
evcs_ac_power_w: '3600',
pv_power_w: '9100', pv_power_10m_avg_w: '6000', pv_trend_w: '0',
house_ac_power_w: '8000', vebus_ac_out_l1_power_w: '8000',
battery_current_a: '-42',
},
expect: {
profile: 'outside_window',
sourceMode: 'solar_only',
state: 'charging_step_down_system_cap',
decision: 'step_down_fast',
desiredCharging: true,
currentA: 18,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'batt-decharge-dure-arret-immediat',
label: "SÉCURITÉ : batterie à -120 A, sous le seuil DUR de la bande (-100 A) → arrêt immédiat, aucune tolérance. La tolérance est du confort, jamais du risque",
time: AFTERNOON,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
evcs_charging_switch: 'on', control_latch: 'on', charge_permission_now: 'on',
last_start_at: '2026-08-08 14:00:00',
soc_percent: '94',
evcs_current_a: '20', evcs_set_current_a: '20', evcs_command_current_a: '20',
evcs_ac_power_w: '3600',
pv_power_w: '1000', pv_power_10m_avg_w: '6000', pv_trend_w: '0',
house_ac_power_w: '9000', vebus_ac_out_l1_power_w: '9000',
battery_current_a: '-120',
},
expect: {
profile: 'outside_window',
sourceMode: 'solar_only',
state: 'battery_current_hard_stop',
decision: 'stop_hard',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'batt-plancher-demarrage-reste-instantane',
label: "FRONTIÈRE : véhicule branché mais à l'arrêt, batterie à -1 A → on NE DÉMARRE PAS sur une batterie qui se décharge. Le plancher de démarrage reste instantané",
time: AFTERNOON,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '94',
pv_power_w: '6600', pv_power_10m_avg_w: '6000', pv_trend_w: '0',
house_ac_power_w: '6000', vebus_ac_out_l1_power_w: '6000',
battery_current_a: '-1',
},
expect: {
profile: 'outside_window',
sourceMode: 'solar_only',
state: 'idle_wait_conditions',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
// ================= STRATÉGIES MANUELLES =================
{
id: 'force-grid-jour',
@@ -1034,7 +1175,7 @@ export const scenarios = [
// le test ne prouverait rien.
{
id: 'etablissement-maintien-charge',
label: 'Réseau fermé il y a 30 s, PV effondrée à 600 W et batterie à -15 A : la charge en cours NE DOIT PAS être arrêtée',
label: 'Réseau fermé il y a 30 s, PV effondrée à 600 W, batterie à -15 A depuis 10 min : dans la fenêtre, la mesure batterie est neutralisée, la charge NE DOIT PAS être arrêtée',
time: MORNING,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
@@ -1047,6 +1188,7 @@ export const scenarios = [
pv_power_w: '600', pv_power_10m_avg_w: '3900', pv_trend_w: '-2000',
grid_state: 'on', victron_acin1_available: '1', grid_power_w: '1800',
battery_current_a: '-15',
battery_soft_breach_at: '2026-08-08 08:50:00',
acin1_last_changed_ts: String(MORNING_TS - 30),
grid_switch_last_changed_ts: String(MORNING_TS - 30),
},
@@ -1064,7 +1206,7 @@ export const scenarios = [
},
{
id: 'etablissement-termine-arret-normal',
label: 'CONTREFACTUEL : mêmes mesures, transition vieille de 400 s → fenêtre fermée, la garde de maintien reprend la main et arrête',
label: 'CONTREFACTUEL : mêmes mesures ET même dépassement de 10 min, transition vieille de 400 s → fenêtre fermée, la garde reprend la main et arrête (10 min > tolérance 4 min de la bande 40_60)',
time: MORNING,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
@@ -1077,6 +1219,7 @@ export const scenarios = [
pv_power_w: '600', pv_power_10m_avg_w: '3900', pv_trend_w: '-2000',
grid_state: 'on', victron_acin1_available: '1', grid_power_w: '1800',
battery_current_a: '-15',
battery_soft_breach_at: '2026-08-08 08:50:00',
acin1_last_changed_ts: String(MORNING_TS - 400),
grid_switch_last_changed_ts: String(MORNING_TS - 400),
},
@@ -1124,7 +1267,7 @@ export const scenarios = [
},
{
id: 'etablissement-ouverture-aucune-fenetre',
label: "ASYMÉTRIE : réseau DISPARU il y a 30 s → aucune fenêtre. Les onduleurs reprennent tout, les gardes doivent agir plus vite, pas moins",
label: "ASYMÉTRIE : réseau DISPARU il y a 30 s → aucune fenêtre. Les onduleurs reprennent tout, les gardes doivent agir plus vite, pas moins (dépassement de 10 min > tolérance 4 min)",
time: MORNING,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
@@ -1138,6 +1281,7 @@ export const scenarios = [
grid_state: 'on', victron_acin1_available: '1', grid_power_w: '1800',
battery_current_a: '-15',
grid_state: 'off', victron_acin1_available: '0', grid_power_w: '0',
battery_soft_breach_at: '2026-08-08 08:50:00',
acin1_last_changed_ts: String(MORNING_TS - 30),
grid_switch_last_changed_ts: String(MORNING_TS - 30),
},

File diff suppressed because one or more lines are too long