#!/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.`);