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

@@ -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.`);