La protection regardait la maison, pas ce qu on demande au parc

Deux fermetures ce dimanche midi. Mesurees a l instant de la decision,
contacteur encore ouvert donc production non censuree : maison 4025 et 4981 W,
mais parc reellement sollicite 1132 et 772 W pour un plafond de 5100. La
decharge batterie mesuree aux memes instants — 1345 et 1223 W — colle a
maison moins PV, pas a la maison brute. Le solaire couvrait la cuisson ; la
regle a ferme le contacteur pour rien, 0,50 kWh importes en plein soleil.

Elle n avait bien fonctionne hier soir que parce qu il n y avait plus de PV.
Les deux seuils portent desormais sur max(0, maison - PV). Contacteur ouvert
cette grandeur EST la decharge batterie ; ferme, un bridage du MPPT la fait
surestimer, donc l erreur pousse a rester ferme et jamais a relacher tot.

Golden 20 -> 24, dont les deux releves de midi et le contrefactuel du nuage.
Le test de mutation a montre que la premiere serie ne couvrait que le seuil de
fermeture : il a fallu ecrire le cas du soleil revenu pour qu elle morde.

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-06 13:50:28 +02:00
parent 66d071684e
commit c7e665cd5c
7 changed files with 341 additions and 1 deletions

View File

@@ -4520,6 +4520,82 @@ qu'apres 12:26.
4. si la tension batterie se remet a disparaitre, `plafond_decharge_w` doit
rester a ~5100 et non tomber a 0.
### MEME JOUR, DEFAUT PLUS PROFOND : la regle comptait la maison BRUTE
Dimanche 06/09, deux fermetures du contacteur : **12:07 -> 12:22** (four +
plaque) et **13:34 -> 13:42** (cuisson + JEEP branchee a 13:31). Journee
nuageuse, parc reduit de moitie. Mesure a la resolution native **a l'instant ou
chaque decision a ete prise, contacteur encore ouvert** — donc sur une
production non censuree :
| fermeture | maison | PV | **parc reellement sollicite** | decharge batterie mesuree |
|---|---:|---:|---:|---:|
| 12:07, cuisson seule | 4 025 W | ~2 890 W | **1 132 W** | 1 345 W |
| 13:34, cuisson + JEEP | 4 981 W | ~4 210 W | **772 W** | 1 223 W |
La decharge batterie colle a `maison - PV`, pas a la maison brute : le controle
croise est net. Le plafond du parc est de 5 100 W — **il etait a 22 % et 15 % de
sa limite, et la regle a ferme le contacteur pour rien.** Meme en integrant les
periodes fermees (ou le PV peut etre bride, donc chiffres pessimistes), la
demande maximale sur le parc n'a atteint que 3 046 et 2 437 W. Cout : 0,50 kWh
importes en plein soleil, pour rien.
Cause : la regle du 05/09 compare la MAISON BRUTE au plafond de decharge du
parc. Elle n'avait bien fonctionne la veille au soir que parce qu'il n'y avait
plus de PV — la maison brute valait alors la demande sur le parc, et le pic de
-97,4 A etait reel. Des qu'il y a du soleil, elle ferme sur une charge que le
solaire couvre deja.
Correctif (deploye 06/09 13:49,
`tools/patch-20260906b-demande-sur-le-parc.mjs`) : les deux seuils portent sur
**`max(0, maison - PV)`**. `acOverSafeLimit` n'est PAS touche — il compare la
charge au debit des onduleurs, qui portent la maison entiere quelle qu'en soit
la source.
⚠️ **Pourquoi ce n'est pas un oscillateur**, alors que la grandeur n'est pas
invariante a la decision (fermer peut brider le MPPT, §28) : contacteur OUVERT,
`maison - PV` EST la decharge batterie, grandeur physique exacte ; contacteur
FERME, un bridage du MPPT fait BAISSER PV donc MONTER `maison - PV` — l'erreur
pousse a rester ferme, jamais a relacher tot ; et le relachement exige de toute
facon le dwell (6 a 20 min), tres au-dela du transitoire d'etablissement mesure
a ~150 s. Erreur unilaterale et bornee, dans la direction qui menage le parc.
Golden 20 -> 24 : les deux releves de ce midi (aucune fermeture attendue), le
contrefactuel du nuage (meme cuisson a 4 500 W avec PV 200 W -> on ferme), et
`h-midi-soleil-revenu-relache` (contacteur ferme, cuisson a 3 000 W mais PV
2 000 -> le parc n'est plus sollicite qu'a 1 000 W, on rend). Les scenarios de
protection existants portent desormais `pv_power_w: '0'` explicitement : ils
decrivent le soir, ou le parc est seul.
⚠️ Piege evite grace au test de mutation : la premiere serie de scenarios ne
couvrait que le seuil de FERMETURE. Remettre le relachement sur la maison brute
ne faisait echouer aucun test — il a fallu ecrire `h-midi-soleil-revenu-relache`
pour que la mutation morde.
### Ce que la JEEP n'a PAS cause, et la question qu'elle pose quand meme
Le propriétaire attribuait la fermeture de midi a la conjonction cuisson +
voiture. Les faits : **la voiture n'etait pas branchee** pendant l'episode de
12:07 (EVCS a 0 W de bout en bout) ; elle n'a joue que dans celui de 13:34, qui
n'aurait pas du fermer non plus. Les deux fermetures viennent de la regle, pas
de la charge.
Reste que la question de fond est juste, et elle n'est pas traitee : `houseAcPowerW`
inclut l'EVCS, donc **la voiture est comptee comme une charge menagere
incompressible** alors qu'elle est la seule charge entierement compressible du
foyer — le controleur EVCS peut la ramener a 6 A ou l'arreter en un cycle. Le
jour ou la demande sur le parc depassera vraiment le plafond avec la voiture
dedans, la reponse economique est de brider la voiture d'abord et de ne fermer
le contacteur que pour le reste. Aucun des deux workflows ne sait faire cela
aujourd'hui : le controleur EVCS ignore le plafond de decharge du BMS, et
l'hysteresis ne sait pas commander la borne. A instruire.
### Anomalie annexe : une session de charge comptee deux fois
`Session de charge terminee : JEEP, 4.20 kWh` a ete emis **deux fois** — a
02:52:31 (fin de la charge HC reelle) et a 13:42:32 (debranchement apres onze
minutes de charge diurne), avec la MEME valeur au centieme. Le compteur de
session de `tools/historise-evcs.mjs` semble rejouer la session de la nuit au
lieu d'en ouvrir une nouvelle. A corriger, sans quoi la table `session_charge`
double-compte.
### Reste ouvert
- Detecteur de gel dans le service d'historisation : une grandeur qui ne bouge
pas alors qu'une autre du meme bilan bouge = alerte. Non fait.

View File

@@ -990,6 +990,36 @@ protection jusqu'a la fin du dwell.
Golden-hysteresis 11 -> 20, six mutations tombant chacune sur un jeu different.
Detail : HANDOFF §57.
#### D-ter. La protection comptait la maison brute, pas la demande sur le parc (06/09/2026)
La regle de fermeture comparait `sensor.victron_consumption_power_l1` au
plafond de decharge du BMS. Or le parc n'est sollicite que de ce que le solaire
ne couvre pas. Dimanche 06/09, deux fermetures, mesurees a l'instant de la
decision (contacteur encore ouvert, donc production non censuree) :
| fermeture | maison | PV | parc sollicite | decharge batterie |
|---|---:|---:|---:|---:|
| 12:07, cuisson | 4 025 W | ~2 890 W | **1 132 W** | 1 345 W |
| 13:34, cuisson + JEEP | 4 981 W | ~4 210 W | **772 W** | 1 223 W |
Plafond du parc : 5 100 W. Il etait a 22 % et 15 % de sa limite — deux
fermetures pour rien, 0,50 kWh importes en plein soleil. La regle n'avait bien
fonctionne la veille au soir que parce qu'il n'y avait plus de PV.
Les deux seuils portent desormais sur `max(0, maison - PV)`. Contacteur ouvert,
cette grandeur EST la decharge batterie ; ferme, elle est le contrefactuel
« ce que le parc devrait fournir si on ouvrait », qu'un eventuel bridage du MPPT
fait SURESTIMER — erreur unilaterale, qui pousse a rester ferme. Golden 20 -> 24.
Detail : HANDOFF §57.
⚠️ Question ouverte que cet episode souleve : `houseAcPowerW` inclut l'EVCS,
donc la voiture est comptee comme une charge incompressible alors qu'elle est la
seule charge entierement compressible du foyer. Le jour ou la demande sur le
parc depassera vraiment le plafond avec la voiture dedans, il faudrait brider la
voiture avant de fermer le contacteur — ce qu'aucun des deux workflows ne sait
faire : le controleur EVCS ignore le plafond de decharge du BMS, et
l'hysteresis ne commande pas la borne.
#### E. Le plancher du soir etait un niveau, pas une energie (03/09/2026)
`evening_soc_start_min` = 85 % tente de dire « garde de quoi passer la nuit ».

View File

@@ -388,3 +388,19 @@ heure_reelle,etat,courant_mA,c1_mV,c2_mV,c3_mV,c4_mV,c5_mV,c6_mV,c7_mV,c8_mV,c9_
2026-09-06T11:01:04.050Z,Y230202C30371426,,3393,3390,3390,3391,3392,3393,3390,3390,3390,3391,3393,3391,3390,3390,3393,3
2026-09-06T11:01:04.145Z,Y230202C30371425,,3392,3390,3390,3390,3391,3391,3391,3389,3389,3391,3393,3392,3391,3391,3391,4
2026-09-06T11:01:04.362Z,Y230202C30371427,,3392,3390,3390,3389,3391,3391,3388,3389,3389,3390,3392,3389,3389,3389,3391,4
2026-09-06T11:11:04.465Z,Y230202C30371426,,3391,3389,3389,3389,3391,3391,3388,3389,3389,3389,3391,3389,3390,3390,3391,3392,3390,3390,3391,3392,3392,3390,3390,3390,3391,3392,3391,3390,3390,3393,5
2026-09-06T11:11:04.593Z,Y230202C30371425,,3390,3389,3389,3390,3389,3390,3389,3388,3388,3390,3391,3390,3390,3390,3390,3
2026-09-06T11:11:04.688Z,Y230202C30371431,,3390,3388,3389,3388,3389,3389,3386,3387,3387,3388,3390,3387,3388,3388,3389,4
2026-09-06T11:11:04.813Z,Y230202C30371427,,3389,3388,3387,3388,3389,3390,3387,3387,3387,3388,3390,3388,3388,3388,3390,3
2026-09-06T11:21:04.908Z,Y230202C30371426,,3391,3389,3389,3390,3390,3391,3389,3389,3388,3389,3392,3390,3389,3388,3391,4
2026-09-06T11:21:05.006Z,Y230202C30371425,,3384,3383,3383,3384,3382,3383,3384,3382,3383,3383,3385,3384,3385,3384,3383,3
2026-09-06T11:21:05.132Z,Y230202C30371431,,3383,3382,3383,3381,3382,3382,3380,3381,3381,3381,3384,3382,3382,3381,3381,4
2026-09-06T11:21:05.257Z,Y230202C30371427,,3383,3382,3382,3383,3384,3383,3382,3382,3383,3382,3383,3383,3383,3384,3384,2
2026-09-06T11:31:05.360Z,Y230202C30371426,,3385,3384,3385,3386,3386,3386,3384,3385,3383,3385,3386,3385,3385,3384,3386,3
2026-09-06T11:31:05.460Z,Y230202C30371425,,3393,3391,3391,3392,3392,3392,3391,3390,3391,3392,3394,3393,3393,3392,3393,4
2026-09-06T11:31:05.587Z,Y230202C30371431,,3393,3391,3391,3390,3393,3392,3389,3390,3390,3391,3394,3390,3390,3390,3391,5
2026-09-06T11:31:05.714Z,Y230202C30371427,,3391,3389,3388,3389,3391,3391,3389,3388,3389,3389,3391,3389,3390,3390,3390,3
2026-09-06T11:41:05.817Z,Y230202C30371426,,3391,3389,3389,3390,3390,3392,3389,3389,3388,3389,3391,3391,3389,3389,3390,4
2026-09-06T11:41:05.944Z,Y230202C30371425,,3350,3350,3350,3350,3350,3349,3350,3349,3349,3350,3350,3350,3350,3350,3350,1
2026-09-06T11:41:06.039Z,Y230202C30371431,,3349,3348,3348,3348,3348,3349,3347,3348,3347,3347,3349,3348,3348,3348,3347,2
2026-09-06T11:41:06.226Z,Y230202C30371427,,3342,3342,3343,3344,3343,3342,3343,3343,3343,3343,3342,3343,3343,3344,3344,2
1 heure_reelle etat courant_mA c1_mV c2_mV c3_mV c4_mV c5_mV c6_mV c7_mV c8_mV c9_mV c10_mV c11_mV c12_mV c13_mV c14_mV c15_mV ecart_mV
388 2026-09-06T11:01:04.050Z Y230202C30371426 3393 3390 3390 3391 3392 3393 3390 3390 3390 3391 3393 3391 3390 3390 3393 3
389 2026-09-06T11:01:04.145Z Y230202C30371425 3392 3390 3390 3390 3391 3391 3391 3389 3389 3391 3393 3392 3391 3391 3391 4
390 2026-09-06T11:01:04.362Z Y230202C30371427 3392 3390 3390 3389 3391 3391 3388 3389 3389 3390 3392 3389 3389 3389 3391 4
391 2026-09-06T11:11:04.465Z Y230202C30371426 3391 3389 3389 3389 3391 3391 3388 3389 3389 3389 3391 3389 3390 3390 3391 3392
392 2026-09-06T11:11:04.593Z Y230202C30371425 3390 3389 3389 3390 3389 3390 3389 3388 3388 3390 3391 3390 3390 3390 3390 3
393 2026-09-06T11:11:04.688Z Y230202C30371431 3390 3388 3389 3388 3389 3389 3386 3387 3387 3388 3390 3387 3388 3388 3389 4
394 2026-09-06T11:11:04.813Z Y230202C30371427 3389 3388 3387 3388 3389 3390 3387 3387 3387 3388 3390 3388 3388 3388 3390 3
395 2026-09-06T11:21:04.908Z Y230202C30371426 3391 3389 3389 3390 3390 3391 3389 3389 3388 3389 3392 3390 3389 3388 3391 4
396 2026-09-06T11:21:05.006Z Y230202C30371425 3384 3383 3383 3384 3382 3383 3384 3382 3383 3383 3385 3384 3385 3384 3383 3
397 2026-09-06T11:21:05.132Z Y230202C30371431 3383 3382 3383 3381 3382 3382 3380 3381 3381 3381 3384 3382 3382 3381 3381 4
398 2026-09-06T11:21:05.257Z Y230202C30371427 3383 3382 3382 3383 3384 3383 3382 3382 3383 3382 3383 3383 3383 3384 3384 2
399 2026-09-06T11:31:05.360Z Y230202C30371426 3385 3384 3385 3386 3386 3386 3384 3385 3383 3385 3386 3385 3385 3384 3386 3
400 2026-09-06T11:31:05.460Z Y230202C30371425 3393 3391 3391 3392 3392 3392 3391 3390 3391 3392 3394 3393 3393 3392 3393 4
401 2026-09-06T11:31:05.587Z Y230202C30371431 3393 3391 3391 3390 3393 3392 3389 3390 3390 3391 3394 3390 3390 3390 3391 5
402 2026-09-06T11:31:05.714Z Y230202C30371427 3391 3389 3388 3389 3391 3391 3389 3388 3389 3389 3391 3389 3390 3390 3390 3
403 2026-09-06T11:41:05.817Z Y230202C30371426 3391 3389 3389 3390 3390 3392 3389 3389 3388 3389 3391 3391 3389 3389 3390 4
404 2026-09-06T11:41:05.944Z Y230202C30371425 3350 3350 3350 3350 3350 3349 3350 3349 3349 3350 3350 3350 3350 3350 3350 1
405 2026-09-06T11:41:06.039Z Y230202C30371431 3349 3348 3348 3348 3348 3349 3347 3348 3347 3347 3349 3348 3348 3348 3347 2
406 2026-09-06T11:41:06.226Z Y230202C30371427 3342 3342 3343 3344 3343 3342 3343 3343 3343 3343 3342 3343 3343 3344 3344 2

View File

@@ -66,6 +66,7 @@ function summarize(out) {
shouldNotify: !!d.should_notify,
evcsGridOverride: !!c.evcs_grid_override,
plafondDechargeW: c.plafond_decharge_w,
demandeParcW: c.demande_parc_w,
dwellMinutes: c.dwell_minutes,
dwellEcoule: c.dwell_ecoule,
reserveDejaCondamnee: !!c.reserve_deja_condamnee,

View File

@@ -63,6 +63,13 @@ const recoveryBase = {
const parcQuatreModules = {
battery_max_discharge_a: '100',
battery_voltage_v: '51',
// ⚠️ PV NULLE, ET C'EST STRUCTUREL. Depuis le 06/09 les deux seuils portent
// sur `max(0, maison - PV)`, ce que le parc est REELLEMENT sollicite de
// fournir, et non sur la maison brute. Les scenarios de protection decrivent
// le soir : le parc est seul, la maison brute EST la demande sur le parc. Les
// scenarios `h-midi-*` couvrent le cas diurne, ou le solaire couvre la
// cuisson et ou la regle ne doit PAS fermer.
pv_power_w: '0',
};
export const scenarios = [
@@ -405,6 +412,7 @@ export const scenarios = [
...recoveryBase,
battery_max_discharge_a: '100',
battery_voltage_v: 'unavailable',
pv_power_w: '0',
soc_percent: '65',
house_ac_power_w: '2000',
last_on_at: minutesAgo(T_MS, 40),
@@ -426,6 +434,7 @@ export const scenarios = [
...recoveryBase,
battery_max_discharge_a: 'unavailable',
battery_voltage_v: 'unavailable',
pv_power_w: '0',
soc_percent: '65',
house_ac_power_w: '2000',
last_on_at: minutesAgo(T_MS, 40),
@@ -447,6 +456,7 @@ export const scenarios = [
...recoveryBase,
battery_max_discharge_a: 'unavailable',
battery_voltage_v: 'unavailable',
pv_power_w: '0',
soc_percent: '65',
house_ac_power_w: '2000',
last_on_at: minutesAgo(T_MS, 40),
@@ -459,4 +469,102 @@ export const scenarios = [
gridAction: 'turn_off',
},
},
// =====================================================
// MIDI ENSOLEILLE : LE SOLAIRE COUVRE LA CUISSON (06/09/2026)
// =====================================================
// Deux fermetures ce dimanche, mesurees a la resolution native A L'INSTANT
// DE LA DECISION, contacteur encore ouvert donc production non censuree :
//
// 12:07, cuisson seule maison 4 025 W parc sollicite 1 132 W
// 13:34, cuisson + JEEP maison 4 981 W parc sollicite 772 W
//
// Controle croise : la decharge batterie valait 1 345 et 1 223 W aux memes
// instants — elle colle a `maison - PV`, pas a la maison brute. Le plafond du
// parc est de 5 100 W : il etait a 22 % et 15 % de sa limite, et la regle a
// ferme le contacteur pour rien.
{
id: 'h-midi-solaire-couvre-la-cuisson',
label: 'RELEVE 06/09 12:07 : maison 4025 W mais PV 2893 W, le parc ne fournit que 1132 W — aucune fermeture',
time: T,
overrides: {
...recoveryBase,
...parcQuatreModules,
grid_latch: 'off',
grid_state: 'off',
soc_percent: '69',
house_ac_power_w: '4025',
pv_power_w: '2893',
},
expect: {
state: 'observe_only',
gridAction: 'none',
latchAction: 'none',
demandeParcW: 1132,
},
},
{
id: 'h-midi-jeep-en-plus-solaire-couvre-encore',
label: 'RELEVE 06/09 13:34 : maison 4981 W dont la JEEP, PV 4209 W, parc a 772 W — aucune fermeture non plus',
time: T,
overrides: {
...recoveryBase,
...parcQuatreModules,
grid_latch: 'off',
grid_state: 'off',
soc_percent: '86',
house_ac_power_w: '4981',
pv_power_w: '4209',
evcs_ac_power_w: '1621',
evcs_charging: 'on',
evcs_surplus_state: 'charging_step_up_battery',
evcs_source_mode: 'Solaire',
},
expect: {
state: 'observe_only',
gridAction: 'none',
demandeParcW: 772,
},
},
{
id: 'h-midi-nuage-le-parc-encaisse-tout',
label: 'CONTREFACTUEL : meme cuisson a 4500 W mais le nuage passe, PV 200 W — le parc encaisse 4300 W, on ferme',
time: T,
overrides: {
...recoveryBase,
...parcQuatreModules,
grid_latch: 'off',
grid_state: 'off',
soc_percent: '69',
house_ac_power_w: '4500',
pv_power_w: '200',
},
expect: {
state: 'turn_on_charge_au_dessus_du_parc',
gridAction: 'turn_on',
latchAction: 'turn_on',
demandeParcW: 4300,
},
},
{
id: 'h-midi-soleil-revenu-relache',
label: "Le nuage est passe, contacteur ferme : la cuisson continue a 3000 W mais le PV en couvre 2000 — le parc n'est plus sollicite qu'a 1000 W, on rend le reseau",
time: T,
overrides: {
...recoveryBase,
...parcQuatreModules,
soc_percent: '69',
house_ac_power_w: '3000',
pv_power_w: '2000',
last_on_at: minutesAgo(T_MS, 40),
charge_high_last_at: minutesAgo(T_MS, 25),
balance_required_soc: '92',
balance_last_run: bilanRunAgo(T_MS, 3),
},
expect: {
state: 'turn_off_vehicle_unplugged',
gridAction: 'turn_off',
demandeParcW: 1000,
setChargeHighLastAt: false,
},
},
];

View File

@@ -0,0 +1,109 @@
#!/usr/bin/env node
// La regle de protection comptait la maison BRUTE, pas ce que le parc est
// reellement sollicite de fournir.
//
// Dimanche 06/09, deux fermetures du contacteur, mesurees a la resolution
// native (InfluxDB) A L'INSTANT OU LA DECISION A ETE PRISE, contacteur encore
// ouvert — donc sur une production non censurée :
//
// fermeture 1 (12:07, cuisson seule) maison 4 025 W parc sollicite 1 132 W
// fermeture 2 (13:34, cuisson + JEEP) maison 4 981 W parc sollicite 772 W
//
// Controle croise : la decharge batterie mesuree aux memes instants valait
// 1 345 et 1 223 W — elle colle a `maison - PV`, pas a la maison brute.
//
// Le plafond du parc est de 5 100 W. Le parc etait donc a 22 % et 15 % de sa
// limite, et la regle a ferme le contacteur pour rien : le solaire (3 a 4 kW)
// couvrait la cuisson. Elle n'avait bien fonctionne hier soir que parce qu'il
// n'y avait plus de PV — la maison brute valait alors la demande sur le parc.
//
// Correctif : les deux seuils portent sur `max(0, maison - PV)`.
//
// ⚠️ POURQUOI CE N'EST PAS UN OSCILLATEUR, alors que la grandeur n'est pas
// invariante a la decision (fermer le contacteur peut brider le MPPT, §28) :
// - contacteur OUVERT, `maison - PV` EST la decharge batterie, la grandeur
// physique exacte ;
// - contacteur FERME, brider le MPPT fait BAISSER PV, donc MONTER
// `maison - PV` : l'erreur pousse a rester ferme, jamais a relacher tot ;
// - et le relachement exige de toute facon le dwell (6 a 20 min), tres
// au-dela du transitoire d'etablissement mesure a ~150 s.
// L'erreur est donc unilaterale et bornee, dans la direction qui menage le parc.
//
// `acOverSafeLimit` n'est PAS touche : il compare la charge au debit des
// onduleurs, qui portent la maison entiere quelle qu'en soit la source.
//
// node tools/patch-20260906b-demande-sur-le-parc.mjs
import fs from 'node:fs';
const F = 'workflows/victron-grid-hysteresis-v1.json';
const wf = JSON.parse(fs.readFileSync(F, 'utf8'));
const calc = wf.nodes.find((n) => n.name === 'Compute Grid Hysteresis');
if (!calc) throw new Error('noeud introuvable');
function remplace(t, a, b, quoi) {
if (!t.includes(a)) throw new Error(`ancre introuvable (${quoi}) : ${a.slice(0, 70)}`);
return t.replace(a, b);
}
let c = calc.parameters.jsCode;
if (c.includes('demandeParcW')) {
console.log(' deja fait');
} else {
// 1. la grandeur
c = remplace(c,
'const chargeAuDessusDuParc = plafondDechargeW > 0 && houseAcPowerW >= seuilFermetureW;',
`// CE QUE LE PARC EST REELLEMENT SOLLICITE DE FOURNIR (06/09/2026)
// La maison brute n'est pas la demande sur la batterie : le solaire en couvre
// une partie. Mesure du 06/09 a l'instant des deux fermetures, contacteur
// encore ouvert : maison 4 025 et 4 981 W, parc sollicite 1 132 et 772 W pour
// un plafond de 5 100 W — deux fermetures pour rien. Controle croise : la
// decharge batterie valait alors 1 345 et 1 223 W.
//
// Contacteur ouvert, cette grandeur EST la decharge batterie. Ferme, elle est
// le contrefactuel « ce que le parc devrait fournir si on ouvrait », et un
// eventuel bridage du MPPT par la presence du reseau la fait SURESTIMER —
// erreur unilaterale, qui pousse a rester ferme, jamais a relacher tot.
const demandeParcW = Math.max(0, houseAcPowerW - pvPowerW);
const chargeAuDessusDuParc = plafondDechargeW > 0 && demandeParcW >= seuilFermetureW;`,
'fermeture');
c = remplace(c,
'const chargeSousSeuilBas = plafondDechargeW > 0 && houseAcPowerW < seuilRelachementW;',
'const chargeSousSeuilBas = plafondDechargeW > 0 && demandeParcW < seuilRelachementW;',
'relachement');
c = remplace(c,
"const setChargeHighLastAt = plafondDechargeW > 0 && !chargeSousSeuilBas;",
"const setChargeHighLastAt = plafondDechargeW > 0 && !chargeSousSeuilBas;",
'ecriture'); // inchange : suit desormais demandeParcW via chargeSousSeuilBas
// 2. les raisons
c = remplace(c,
' `Charge maison ${Math.round(houseAcPowerW)}W au-dessus de ${Math.round(seuilFermetureW)}W `\n + `(80% du plafond de decharge BMS ${Math.round(plafondDechargeW)}W) : le reseau prend la difference`);',
' `Demande sur le parc ${Math.round(demandeParcW)}W (maison ${Math.round(houseAcPowerW)}W - PV ${Math.round(pvPowerW)}W) `\n + `au-dessus de ${Math.round(seuilFermetureW)}W, soit 80% du plafond de decharge BMS ${Math.round(plafondDechargeW)}W : le reseau prend la difference`);',
'raison fermeture');
c = remplace(c,
" ? `Charge maison retombee a ${Math.round(houseAcPowerW)}W depuis ${round1(minutesDepuisChargeHaute || 0)} min : `",
" ? `Demande sur le parc retombee a ${Math.round(demandeParcW)}W depuis ${round1(minutesDepuisChargeHaute || 0)} min : `",
'raison dwell');
c = remplace(c,
" : `Charge maison ${Math.round(houseAcPowerW)}W encore au-dessus de ${Math.round(seuilRelachementW)}W : on garde le reseau`);",
" : `Demande sur le parc ${Math.round(demandeParcW)}W (maison ${Math.round(houseAcPowerW)}W - PV ${Math.round(pvPowerW)}W) encore au-dessus de ${Math.round(seuilRelachementW)}W : on garde le reseau`);",
'raison maintien');
// 3. le debug
c = remplace(c, ' plafond_decharge_w: Math.round(plafondDechargeW),',
' plafond_decharge_w: Math.round(plafondDechargeW),\n demande_parc_w: Math.round(demandeParcW),',
'debug');
calc.parameters.jsCode = c;
console.log(' calcul : les deux seuils portent sur maison - PV');
}
fs.writeFileSync(F, JSON.stringify(wf, null, 2) + '\n');
console.log(`\n${F} ecrit.`);

File diff suppressed because one or more lines are too long