Session reseau unique de la nuit : engagement sur le bilan, sortie sur la fenetre

§26 avait pose l objectif et designe le critere. Avant d ecrire quoi que ce
soit, j ai mesure les kWh reellement sortis de la batterie par point de SOC sur
cinq fenetres et trois nuits : 0,164 a 0,171 kWh/point au-dessus de 42 %, soit
environ 17 kWh, mais 0,121 entre 45 et 22 % et 0,082 entre 33 et 20 %. En bas
d echelle un point de SOC vaut moitie moins, alors que BALANCE.js convertit avec
une constante unique de 15 kWh. required_soc y est donc systematiquement trop
bas — c est le facteur 1,75 mesure la nuit du 02-03/09, ou le SOC descendait
bien plus vite que la ligne.

Cette mesure a dicte la conception : le relachement de la session ne peut pas
etre un seuil de SOC, puisqu il porterait sur une grandeur fausse et que tout
seuil que le SOC repasse ensuite rouvre l oscillateur du 27/08. Les deux
variantes evidentes — relacher sur required_soc, ou sur required_soc_prudent
avec la bande p50/p80 — ont ete ecartees pour cette raison.

Retenu : engagement sur le bilan (batterie sous la cible), relachement a la fin
de la fenetre. L oscillation devient structurellement impossible, la condition
de sortie etant monotone : une fenetre ne se ferme qu une fois. Un nouveau
helper input_boolean.victron_evcs_night_grid_session_committed porte
l engagement, cree par input_boolean.reload sans redemarrage HA et ecrit
seulement sur changement reel ; sans cette persistance la garde redeviendrait
une bande, donc un oscillateur.

Effet : l ACM recharge jusqu a la cible puis tient la batterie a 2 A, la maison
finit la nuit sur le reseau en heures creuses au lieu de vider la reserve, et la
batterie arrive a l aube pres de sa cible avec de la place pour le solaire. La
coupure immediate E2 de l hysteresis exige l absence d override et le helper
takeover reste ON : aucune modification du workflow d hysteresis.

Golden 75 -> 78. Le scenario epingle quelques heures plus tot comme limite
connue devient celui du correctif, attendu mis a jour dans le meme commit.
Quatre mutations ; la plus parlante retire la garde bilan et fait tomber les
trois scenarios anti-oscillateur du 27/08 — les tests de l ancien correctif
protegent le nouveau code.

Prerequis identifie et non traite : la conversion kWh vers points de SOC est une
constante la ou la realite est une courbe. Consequence, un sous-declenchement
possible en bas d echelle, jamais une oscillation. Prochain chantier du bilan.

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-03 23:14:49 +02:00
parent 05bdf019c2
commit 914ef021b2
11 changed files with 334 additions and 23 deletions

View File

@@ -2923,3 +2923,122 @@ oscillateurs, et merite ses propres scenarios.
---
*Document mis a jour le : 03/09/2026 (~22:55) — cible du matin issue du bilan energetique, golden 75/75, deploye*
## 40. Session du 03/09/2026 (suite) — Session reseau UNIQUE de la nuit
### La mesure qui a dicte la conception (a lire avant tout le reste)
Avant d'ecrire une ligne, j'ai mesure les kWh reellement sortis de la batterie
par point de SOC, sur cinq fenetres et trois nuits :
| fenetre | bande SOC | kWh/point | capacite implicite |
|---|---|---:|---:|
| 03/09 21:30-22:50 | 74 → 57 | 0,171 | **17,1 kWh** |
| 31/08 03:00-05:00 | 49 → 42 | 0,170 | **17,0 kWh** |
| 03/09 19:44-20:17 | 94 → 85 | 0,164 | **16,4 kWh** |
| 02-03/09 03:00-06:00 (zero reseau) | 45 → 22 | 0,121 | **12,1 kWh** |
| 01-02/09 03:00-06:00 | 33 → 20 | 0,082 | **8,2 kWh** |
Au-dessus de ~42 % la jauge est lineaire et coherente (~17 kWh, ce qui valide
le `batteryUsableCapacityKwh: 17.5` code en dur). **En dessous elle s'effondre :
un point de SOC y vaut moitie moins.** Or `BALANCE.js` convertit avec une
constante unique : `socNeeded = energyToCover / usable_capacity_kwh * 100`, avec
le helper a 15 kWh. `required_soc` est donc systematiquement TROP BAS dans la
bande ou la nuit se termine — c'est le facteur 1,75 mesure la nuit du 02-03/09,
ou le SOC descendait bien plus vite que la ligne.
**Conclusion de conception, et elle est structurante : le relachement de la
session reseau ne peut PAS etre un seuil de SOC.** Il porterait sur une grandeur
fausse, et tout seuil que le SOC repasse ensuite rouvre l'oscillateur eteint le
27/08. C'est ce raisonnement qui a ecarte les deux variantes evidentes
(relacher sur `required_soc`, ou sur `required_soc_prudent` avec la bande p50/p80).
### Le correctif (deploye 03/09 23:13, golden 78/78)
**ENGAGEMENT sur le bilan, RELACHEMENT a la FIN DE LA FENETRE.**
```js
nightGridSessionUncovered = balanceMorningTargetApplied && batteryBelowMorningTarget;
nightGridSessionCommitted = nightSessionActive && (uncovered || committedPrev);
nightGridTakeoverNeeded = nightSessionActive && (nightGridSessionCommitted || ...);
```
L'oscillation devient **structurellement impossible** : la condition de sortie
est monotone — une fenetre ne se ferme qu'une fois. Aucun seuil de
re-engagement n'existe, donc aucun cycle limite ne peut s'installer.
Nouveau helper **`input_boolean.victron_evcs_night_grid_session_committed`**
(paquet `victron_evcs_night_helpers.yaml`, sauvegarde dans `packages/SAVE/`,
cree par `input_boolean.reload` — **aucun redemarrage HA**, meme procede que le
01/09). Ecrit uniquement sur changement reel. **C'est ce bit qui rend la session
unique** : sans persistance, l'engagement serait reevalue a chaque cycle et le
systeme retomberait dans une bande, donc dans un oscillateur.
### Ce que ca fait concretement
L'ACM recharge jusqu'a la cible puis tient la batterie a 2 A
(`evcs_takeover_hold`) : la maison finit la nuit **sur le reseau en heures
creuses** au lieu de vider la reserve, et la batterie arrive a l'aube pres de sa
cible — donc avec de la place pour le solaire du matin. A comparer au 03/09 :
batterie a 20 %, reseau encore ouvert a 10:33, et 1,49 kWh achetes au plein
tarif entre 07:24 et 10:33 (pendant lesquels le reseau evincait en plus la
production naissante).
### Composition avec l'hysteresis — verifiee
La coupure immediate E2 (`turn_off_recovery_done`) exige `!evcsGridOverride`, et
`evcsExplicitGridRequest` inclut `evcsNightGridTakeoverNeeded`. Tant que la
session est engagee, le helper takeover est ON, donc E2 ne s'applique pas et la
boucle generique maintient le reseau. **Aucune modification du workflow
d'hysteresis n'a ete necessaire.**
### Golden 75 → 78 (et un scenario promu)
`cible-bilan-limite-passe-unique`, epingle quelques heures plus tot comme LIMITE
CONNUE, est devenu `session-unique-tient-sous-la-ligne` : c'est exactement ce
que ce correctif change. Attendu mis a jour dans le meme commit, conformement a
la regle du depot.
| scenario | role |
|---|---|
| `session-unique-tient-sous-la-ligne` | SOC 30 sous le requis 37,1 → engagement |
| `session-unique-ne-relache-pas-sur-le-soc` | **preuve anti-oscillateur** : SOC 45 repasse AU-DESSUS, la session tient et rien n'est reecrit |
| `session-unique-fin-de-fenetre` | la seule sortie : 08:00, hors fenetre → relache + bit nettoye |
| `session-unique-bilan-indisponible` | bilan perime → aucun engagement, comportement historique |
Mutation, quatre directions. La plus instructive est la quatrieme : **retirer la
garde bilan fait tomber les TROIS scenarios anti-oscillateur du 27/08**
(`night-oscillateur-nuit-2627`, `night-recovery-done-pas-de-reengagement`,
`night-recovery-done-bande-collante`). Les tests de l'ancien correctif protegent
activement le nouveau code — c'est exactement ce qu'on attend d'une suite golden
apres cinq semaines d'accumulation.
### ⚠️ PREREQUIS IDENTIFIE, NON TRAITE — la conversion kWh ↔ SOC
Le tableau en tete de cette section est un resultat a part entiere, et il
depasse ce correctif : **`evcs_balance_usable_capacity_kwh` = 15 est une
constante la ou la realite est une courbe** (17 kWh au-dessus de 42 %, 8 a
12 kWh en dessous), et `INPUT_PARSE` code 17,5 en dur de son cote. Tant que
cette conversion est lineaire :
- `required_soc` sous-estime le SOC necessaire en bas d'echelle ;
- donc l'engagement de la session peut manquer des nuits ou il aurait fallu
s'engager (sous-declenchement — jamais d'oscillation, la conception y est
immune) ;
- et la reserve d'orage (§26) est optimiste dans la meme bande.
C'est le prochain chantier du bilan energetique. §28 avait deja signale
l'effondrement de la jauge en bas d'echelle et conclu « a verser au chantier du
bilan energetique ; ne pas corriger par un seuil de plus » — les mesures
ci-dessus lui donnent maintenant sa courbe.
### Limite de perimetre
La session ne s'engage que pendant une **session nuit** (fenetre HC ET vehicule
branche) : c'est la portee du contrôleur EVCS. Une nuit sans vehicule reste
geree par l'hysteresis seule, avec ses seuils fixes 20/30 (§11.8.A) — chantier
distinct.
### Etat du cablage du bilan
| consommateur | statut |
|---|---|
| plancher de demarrage du soir | ✅ §38 |
| cible du matin | ✅ §39 |
| session reseau unique de la nuit | ✅ §40 |
| conversion kWh ↔ SOC (prerequis) | ❌ constante 15 kWh vs courbe mesuree |
| hysteresis `grid_soc_on`/`off` (20/30) | ❌ niveaux fixes |
---
*Document mis a jour le : 03/09/2026 (~23:20) — session reseau unique de la nuit, golden 78/78, deploye*

View File

@@ -867,6 +867,51 @@ Le bon critere de re-engagement — « un budget d'energie restant a couvrir »
etait deja designe par §26 ; le bilan est maintenant cable, mais la suite touche
la machine a etats de la nuit et fera l'objet d'un chantier propre.
#### G. Session reseau unique de la nuit (03/09/2026)
Le chantier annonce ci-dessus, fait le soir meme — mais pas comme prevu.
**Mesure prealable, qui a change la conception.** Les kWh reellement sortis de
la batterie par point de SOC, sur cinq fenetres et trois nuits : 0,164 a
0,171 kWh/point au-dessus de 42 % (soit ~17 kWh), mais **0,121 entre 45 et 22 %
et 0,082 entre 33 et 20 %**. En bas d'echelle, un point de SOC vaut moitie
moins. Or le bilan convertit avec une constante unique de 15 kWh :
`required_soc` y est donc systematiquement trop bas — d'ou la batterie qui,
la nuit du 02-03/09, descendait 1,75 fois plus vite que la ligne.
**Consequence : le relachement ne peut pas etre un seuil de SOC.** Il porterait
sur une grandeur fausse, et tout seuil que le SOC repasse ensuite rouvre
l'oscillateur du 27/08. Les deux variantes evidentes — relacher sur
`required_soc`, ou sur `required_soc_prudent` avec la bande p50/p80 — ont ete
ecartees pour cette raison.
**Conception retenue : engagement sur le bilan, relachement a la FIN DE LA
FENETRE.** L'oscillation devient structurellement impossible, la condition de
sortie etant monotone : une fenetre ne se ferme qu'une fois. Un helper
`input_boolean.victron_evcs_night_grid_session_committed` porte l'engagement —
sans cette persistance, la garde serait reevaluee a chaque cycle et redeviendrait
une bande.
Effet : l'ACM recharge jusqu'a la cible puis tient la batterie a 2 A, la maison
finit la nuit sur le reseau **en heures creuses** au lieu de vider la reserve, et
la batterie arrive a l'aube pres de sa cible, avec de la place pour le solaire.
La coupure immediate E2 de l'hysteresis exige `!evcsGridOverride` et le helper
takeover reste ON : aucune modification du workflow d'hysteresis n'a ete
necessaire.
Golden 75 → 78. La mutation la plus parlante : retirer la garde bilan fait
tomber les trois scenarios anti-oscillateur du 27/08 — les tests de l'ancien
correctif protegent le nouveau code. Detail : HANDOFF §40.
⚠️ **Prerequis identifie, non traite** : `evcs_balance_usable_capacity_kwh` = 15
est une constante la ou la realite est une courbe, et `INPUT_PARSE` code 17,5 en
dur de son cote. Consequence : `required_soc` sous-estime le besoin en bas
d'echelle, donc la session peut manquer des nuits ou elle aurait du s'engager
(sous-declenchement — jamais d'oscillation, la conception y est immune), et la
reserve d'orage est optimiste dans la meme bande. §28 avait deja signale
l'effondrement de la jauge ; les mesures du 03/09 lui donnent sa courbe. C'est
le prochain chantier du bilan energetique.
⚠️ Point structurel mis au jour par cet incident : le bilan energetique publie
l'heure de croisement, l'energie a couvrir, le SOC requis, `grid_needed` et
`pv_censored` — et **aucun des deux workflows de decision n'en lit une seule

View File

@@ -362,6 +362,7 @@ const batteryMorningTargetPct =
: params.evcsNightTargetWeakForecastPct;
const advisorNightTargetApplied = !!nightPrev.advisorNightTargetApplied;
const balanceMorningTargetApplied = !!nightPrev.balanceMorningTargetApplied;
const nightGridSessionCommitted = !!nightPrev.nightGridSessionCommitted;
const maxBatteryDischargePoints =
Number.isFinite(nightPrev.maxBatteryDischargePoints) ? nightPrev.maxBatteryDischargePoints : 0;
const gridTakeoverTriggerPct =
@@ -1082,6 +1083,7 @@ return [{
batteryMorningTargetPct,
advisorNightTargetApplied,
balanceMorningTargetApplied,
nightGridSessionCommitted,
maxBatteryDischargePoints,
gridTakeoverTriggerPct,

View File

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

View File

@@ -986,6 +986,19 @@ return [{
}
},
// SESSION RESEAU UNIQUE (03/09/2026) : ce bit est ce qui rend la session
// UNIQUE. Sans persistance, l'engagement serait reevalue a chaque cycle
// et le systeme retomberait dans une bande — donc dans un oscillateur.
// Ecrit uniquement sur changement reel.
helper_night_grid_session_committed: {
execute: night.nightGridSessionCommitted !== history.nightGridSessionCommittedPrev,
domain: 'input_boolean',
service: night.nightGridSessionCommitted ? 'turn_on' : 'turn_off',
data: {
entity_id: 'input_boolean.victron_evcs_night_grid_session_committed'
}
},
helper_night_grid_takeover_needed: {
execute: true,
domain: 'input_boolean',

View File

@@ -132,6 +132,7 @@
"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_grid_session_committed_prev": states("input_boolean.victron_evcs_night_grid_session_committed"),
"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"),

View File

@@ -568,6 +568,8 @@ const history = {
// 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.
nightGridSessionCommittedPrev:
(input.night_grid_session_committed_prev ?? 'off').toString().trim().toLowerCase() === 'on',
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

@@ -561,9 +561,57 @@ const batteryAssistAllowed =
!nightTakeoverLatched &&
!batteryRecoveryUrgent;
// =====================================================
// SESSION RESEAU UNIQUE DE LA NUIT (03/09/2026)
// =====================================================
// §26 avait pose l'objectif — « une session reseau unique et dimensionnee » —
// et designe le bon critere : « un budget d'energie restant a couvrir, il
// appartient au bilan energetique ». Le bilan est cable depuis ce soir.
//
// CE QUI A ETE MESURE AVANT D'ECRIRE CECI, et qui a dicte la conception :
// la conversion kWh -> points de SOC du bilan est une constante unique
// (15 kWh). La jauge reelle vaut ~17 kWh au-dessus de 42 % (0,164 a
// 0,171 kWh/point sur trois fenetres independantes) mais s'effondre en
// dessous : 12,1 kWh entre 45 et 22 %, 8,2 kWh entre 33 et 20 %. Un point de
// SOC vaut donc MOITIE MOINS dans la bande ou la nuit se termine, et
// `required_soc` y est systematiquement trop BAS — c'est pourquoi la batterie
// est restee sous la ligne toute la nuit du 02-03/09, en descendant 1,75 fois
// plus vite qu'elle.
//
// CONSEQUENCE DE CONCEPTION : le relachement ne doit PAS etre un seuil de SOC.
// Il porterait sur une grandeur fausse, et tout seuil que le SOC repasse
// ensuite rouvre l'oscillateur eteint le 27/08.
//
// Donc : ENGAGEMENT sur le bilan (batterie sous la cible), RELACHEMENT a la
// FIN DE LA FENETRE. L'oscillation devient structurellement impossible : la
// condition de sortie est monotone, une fenetre ne se ferme qu'une fois. Le
// bit d'engagement est porte par
// input_boolean.victron_evcs_night_grid_session_committed — sans lui le
// systeme reevaluerait a chaque cycle et retomberait dans une bande.
//
// CE QUE CA FAIT CONCRETEMENT : l'ACM recharge jusqu'a la cible puis tient la
// batterie a 2 A (evcs_takeover_hold) ; la maison finit la nuit sur le reseau
// en HEURES CREUSES au lieu de vider la reserve, et la batterie arrive a
// l'aube pres de sa cible, donc avec de la place pour le solaire du matin —
// au lieu d'etre a 20 % avec le reseau encore ouvert a 10:33 (03/09).
//
// PORTEE : uniquement quand le bilan est exploitable, et uniquement pendant
// une session nuit (fenetre HC + vehicule branche). Sinon, comportement
// historique inchange. Le bit se nettoie tout seul a la fin de la session.
const nightGridSessionCommittedPrev =
(input.night_grid_session_committed_prev ?? 'off').toString().trim().toLowerCase() === 'on';
const nightGridSessionUncovered =
balanceMorningTargetApplied && batteryBelowMorningTarget;
const nightGridSessionCommitted =
nightSessionActive &&
(nightGridSessionUncovered || nightGridSessionCommittedPrev);
const nightGridTakeoverNeeded =
nightSessionActive &&
(
nightGridSessionCommitted ||
nightGridHcPreferred ||
batteryBelowHardFloor ||
(
@@ -959,11 +1007,12 @@ return [{
batteryMorningTargetPct,
batteryMorningTargetBasePct,
balanceMorningTargetApplied,
nightGridSessionCommitted,
nightGridSessionUncovered,
balanceTargetAgeMin:
balanceTargetAgeMin === null ? null : Math.round(balanceTargetAgeMin * 10) / 10,
batteryMorningTargetRulePct,
advisorNightTargetApplied,
balanceMorningTargetApplied,
maxBatteryDischargePoints,
gridTakeoverTriggerPct,

View File

@@ -116,6 +116,10 @@ function summarize(out) {
takeoverNeeded: !!c.night_grid_takeover_needed,
edfOnAction: !!calls.edf_general_ok_on?.execute,
takeoverHelperService: calls.helper_night_grid_takeover_needed?.service || null,
// Session reseau unique (03/09/2026) : null = aucune ecriture ce cycle.
sessionCommittedService: calls.helper_night_grid_session_committed?.execute
? calls.helper_night_grid_session_committed.service
: null,
// Consigne de puissance reseau ecrite ce cycle (null = aucune ecriture).
gridSetpointW: calls.grid_power_setpoint?.execute
? calls.grid_power_setpoint.data.value

View File

@@ -1143,22 +1143,20 @@ export const scenarios = [
},
},
// ===== 03/09/2026 : SESSION RESEAU UNIQUE DE LA NUIT =====
// Engagement sur le bilan, relachement a la FIN DE LA FENETRE — jamais sur
// un seuil de SOC. Deux raisons, toutes deux mesurees le 03/09 : un seuil de
// relachement que le SOC repasse ensuite rouvre l'oscillateur du 27/08, et
// il porterait de toute facon sur une grandeur fausse (la conversion
// kWh -> points du bilan est lineaire a 15 kWh alors que la jauge vaut
// ~17 kWh au-dessus de 42 % et 8 a 12 kWh en dessous).
//
// Le premier scenario etait, quelques heures plus tot, epingle comme LIMITE
// CONNUE : le reseau restait relache alors que le SOC etait repasse sous la
// ligne. C'est exactement ce que ce correctif change.
{
// LIMITE CONNUE, FIGEE VOLONTAIREMENT (03/09/2026).
// La passe unique de recovery (27/08, anti-oscillateur) latche
// `nightRecoveryDone` : une fois la cible atteinte, le reseau est relache
// DEFINITIVEMENT pour la session, meme si le SOC repasse ensuite sous la
// ligne du bilan. La cible dynamique n'ameliore donc que la PREMIERE passe.
// Ce scenario epingle ce comportement pour qu'un changement futur soit
// deliberе et non accidentel. Filet restant : l'hysteresis a SOC <= 20 %,
// mais elle peut tomber hors heures creuses (c'est ce qui a coute 1,49 kWh
// au plein tarif le 03/09 entre 07:24 et 10:33).
// §26 designait deja le bon critere de re-engagement : « un budget
// d'energie restant a couvrir — il appartient au bilan energetique ».
// Le bilan est desormais cable ; la suite est un chantier a part, car elle
// touche la machine a etats de la nuit.
id: 'cible-bilan-limite-passe-unique',
label: "LIMITE FIGÉE : recovery déjà latchée, SOC 30 repasse sous le requis 37,1 % → le réseau reste relâché (passe unique, §26)",
id: 'session-unique-tient-sous-la-ligne',
label: "Nuit HC 05:30, recovery déjà latchée, SOC 30 sous le requis 37,1 % → la session réseau s'ENGAGE et tient",
time: '2026-08-08T05:30:00+02:00',
overrides: {
...VEHICLE_NIGHT, allow_grid_hc_night: 'on',
@@ -1172,9 +1170,81 @@ export const scenarios = [
},
expect: {
profile: 'night_hc',
state: 'night_recovery_done_idle',
takeoverNeeded: true,
takeoverHelperService: 'turn_on',
sessionCommittedService: 'turn_on',
},
},
{
// LA PREUVE ANTI-OSCILLATEUR. Une fois engagee, la session ne se relache
// PAS parce que le SOC est repasse au-dessus de la ligne. Sans ce
// scenario, un relachement sur seuil passerait les tests et recreerait le
// cycle de 31 min corrige le 27/08.
id: 'session-unique-ne-relache-pas-sur-le-soc',
label: "Session déjà engagée, SOC 45 repassé AU-DESSUS du requis 37,1 % → le réseau reste tenu, et rien n'est réécrit",
time: '2026-08-08T05:30:00+02:00',
overrides: {
...VEHICLE_NIGHT, 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',
night_grid_session_committed_prev: 'on',
soc_percent: '45',
balance_required_soc: '37.1', balance_energy_to_cover_kwh: '2.3',
balance_status: 'ok', balance_last_run: '08/08 05:28',
},
expect: {
profile: 'night_hc',
takeoverNeeded: true,
takeoverHelperService: 'turn_on',
sessionCommittedService: null,
},
},
{
// LA SORTIE, et la seule : la fenetre se ferme. Condition monotone, donc
// aucune oscillation possible.
id: 'session-unique-fin-de-fenetre',
label: "08:00, hors fenêtre HC, session encore marquée engagée → réseau relâché et bit nettoyé",
time: '2026-08-08T08:00:00+02:00',
overrides: {
...VEHICLE_NIGHT, allow_grid_hc_night: 'on',
grid_state: 'on', victron_acin1_available: '1',
battery_current_a: '2', night_takeover_prev: 'on',
evcs_charging_switch: 'off',
night_grid_session_committed_prev: 'on',
soc_percent: '45',
balance_required_soc: '37.1', balance_energy_to_cover_kwh: '2.3',
balance_status: 'ok', balance_last_run: '08/08 07:58',
},
expect: {
takeoverNeeded: false,
takeoverHelperService: 'turn_off',
sessionCommittedService: 'turn_off',
},
},
{
id: 'session-unique-bilan-indisponible',
label: "Même relevé qu'en tête, bilan périmé → aucun engagement, comportement historique (réseau relâché)",
time: '2026-08-08T05:30:00+02:00',
overrides: {
...VEHICLE_NIGHT, allow_grid_hc_night: 'on',
grid_state: 'off', victron_acin1_available: '0',
battery_current_a: '-14', night_takeover_prev: 'off',
evcs_charging_switch: 'off',
prev_surplus_state: 'night_recovery_done_idle',
soc_percent: '30',
balance_required_soc: '37.1', balance_energy_to_cover_kwh: '2.3',
balance_status: 'ok', balance_last_run: '08/08 03:00',
},
expect: {
profile: 'night_hc',
takeoverNeeded: false,
takeoverHelperService: 'turn_off',
sessionCommittedService: null,
},
},

File diff suppressed because one or more lines are too long