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
110 lines
5.5 KiB
JavaScript
110 lines
5.5 KiB
JavaScript
#!/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.`);
|