Le reseau vient quand la maison demande plus que la batterie ne sait donner
Quatre modules : le BMS annonce 100 A de decharge sous 51 V, soit 5,1 kW, quand la maison a deja pointe a 8 kW en cuisinant. Contacteur ouvert, la batterie est seule et se fait tirer au-dela de sa limite. Le declenchement porte sur la CHARGE MAISON, jamais sur le courant batterie : fermer le contacteur fait chuter ce courant, donc s'en servir comme critere de relachement ferait que la decision detruirait sa propre cause. Fermeture a 80 % du plafond, relachement sous 50 %, les deux seuils derives du plafond annonce par le BMS - ils suivront donc le parc sans qu'on y touche. Les tests ont revele un vrai defaut au passage : les trois coupures immediates s'executent avant la cascade et auraient ouvert le contacteur que la regle venait de fermer. Elles cedent desormais devant la charge maison. Preuve par mutation : sans cette garde, un scenario tombe. Plafond illisible => regle inerte, comportement historique intact. 11/11. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
This commit is contained in:
@@ -46,7 +46,81 @@ const recoveryBase = {
|
||||
user_strategy: 'auto_solar_plus',
|
||||
};
|
||||
|
||||
// Plafond de decharge REEL depuis le retrait du pack ancien (05/09/2026) :
|
||||
// quatre modules, 100 A annonces par le BMS sous 51 V, soit 5100 W. Les seuils
|
||||
// en decoulent — 4080 W pour fermer, 2550 W pour relacher — et se deplaceront
|
||||
// tout seuls le jour ou le parc changera.
|
||||
const parcQuatreModules = {
|
||||
battery_max_discharge_a: '100',
|
||||
battery_voltage_v: '51',
|
||||
};
|
||||
|
||||
export const scenarios = [
|
||||
{
|
||||
id: 'h-charge-au-dessus-du-parc-ferme',
|
||||
label: 'Cuisson a 4500 W contre 5100 W de plafond BMS : le reseau vient prendre la difference',
|
||||
time: T,
|
||||
overrides: {
|
||||
...recoveryBase,
|
||||
...parcQuatreModules,
|
||||
grid_latch: 'off',
|
||||
grid_state: 'off',
|
||||
soc_percent: '91',
|
||||
house_ac_power_w: '4500',
|
||||
},
|
||||
expect: {
|
||||
state: 'turn_on_charge_au_dessus_du_parc',
|
||||
gridAction: 'turn_on',
|
||||
latchAction: 'turn_on',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'h-charge-encore-elevee-maintient',
|
||||
label: 'Charge retombee a 3000 W, entre les deux seuils : on garde le reseau au lieu de le lacher',
|
||||
time: T,
|
||||
overrides: {
|
||||
...recoveryBase,
|
||||
...parcQuatreModules,
|
||||
soc_percent: '91',
|
||||
house_ac_power_w: '3000',
|
||||
last_on_at: minutesAgo(T_MS, 40),
|
||||
},
|
||||
expect: {
|
||||
state: 'hold_on_charge_encore_elevee',
|
||||
gridAction: 'none',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'h-charge-retombee-relache',
|
||||
label: 'CONTREFACTUEL : meme situation a 2000 W, sous le seuil de relachement — le reseau est rendu (ici par Fix C, le vehicule etant debranche)',
|
||||
time: T,
|
||||
overrides: {
|
||||
...recoveryBase,
|
||||
...parcQuatreModules,
|
||||
soc_percent: '91',
|
||||
house_ac_power_w: '2000',
|
||||
last_on_at: minutesAgo(T_MS, 40),
|
||||
},
|
||||
expect: {
|
||||
state: 'turn_off_vehicle_unplugged',
|
||||
gridAction: 'turn_off',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'h-plafond-inconnu-aucune-action',
|
||||
label: 'Plafond BMS illisible : la regle est inerte, le comportement historique tient',
|
||||
time: T,
|
||||
overrides: {
|
||||
...recoveryBase,
|
||||
soc_percent: '91',
|
||||
house_ac_power_w: '4500',
|
||||
last_on_at: minutesAgo(T_MS, 40),
|
||||
},
|
||||
expect: {
|
||||
state: 'turn_off_vehicle_unplugged',
|
||||
gridAction: 'turn_off',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'h-engage-low-soc',
|
||||
label: 'SOC 18% <= seuil ON 20% : EDF maintenu (engagement batterie basse)',
|
||||
|
||||
131
tools/patch-20260905-decharge-forte-ferme-edf.mjs
Normal file
131
tools/patch-20260905-decharge-forte-ferme-edf.mjs
Normal file
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env node
|
||||
// Ferme le contacteur EDF quand la maison demande plus que ce que la batterie
|
||||
// sait delivrer.
|
||||
//
|
||||
// Le 05/09/2026 le pack ancien a ete isole : quatre modules au lieu de sept, et
|
||||
// le plafond de decharge annonce par le BMS est tombe de 150 a 100 A, soit
|
||||
// ~5,1 kW. La maison, elle, a deja pointe a 8 kW en cuisinant. Contacteur
|
||||
// ouvert, la batterie est seule source et se fait tirer au-dela de sa limite.
|
||||
//
|
||||
// ⚠️ LE DECLENCHEMENT PORTE SUR LA CHARGE MAISON, PAS SUR LE COURANT BATTERIE.
|
||||
// Fermer le contacteur fait chuter le courant batterie : s'en servir comme
|
||||
// critere de relachement ferait que la decision detruirait sa propre cause —
|
||||
// exactement l'oscillateur corrige trois fois cette annee. La charge maison ne
|
||||
// depend pas de l'etat du contacteur : c'est l'invariant qu'il faut regarder.
|
||||
//
|
||||
// node tools/patch-20260905-decharge-forte-ferme-edf.mjs
|
||||
|
||||
import fs from 'node:fs';
|
||||
|
||||
const F = 'workflows/victron-grid-hysteresis-v1.json';
|
||||
const wf = JSON.parse(fs.readFileSync(F, 'utf8'));
|
||||
const snap = wf.nodes.find((n) => n.name === 'HA Snapshot Grid Hysteresis');
|
||||
const calc = wf.nodes.find((n) => n.name === 'Compute Grid Hysteresis');
|
||||
if (!snap || !calc) throw new Error('noeuds introuvables');
|
||||
|
||||
// ------------------------------------------------------- 1. le snapshot
|
||||
let p = JSON.stringify(snap.parameters);
|
||||
if (!p.includes('battery_max_discharge_a')) {
|
||||
const ancre = 'house_ac_power_w\\": states(\\"sensor.victron_consumption_power_l1\\"),';
|
||||
if (!p.includes(ancre)) throw new Error('ancre snapshot introuvable');
|
||||
p = p.replace(ancre, ancre
|
||||
+ '\\n \\"battery_max_discharge_a\\": states(\\"sensor.airtria_8_us2000c_maximum_allowed_discharge_current\\"),'
|
||||
+ '\\n \\"battery_voltage_v\\": states(\\"sensor.victron_battery_voltage\\"),');
|
||||
snap.parameters = JSON.parse(p);
|
||||
console.log(' snapshot : +battery_max_discharge_a, +battery_voltage_v');
|
||||
} else console.log(' snapshot : deja fait');
|
||||
|
||||
// ------------------------------------------------------- 2. les grandeurs
|
||||
let c = calc.parameters.jsCode;
|
||||
const ancreCalc = 'const acOverSafeLimit = houseAcPowerW >= acLoadsMaxSafeW;';
|
||||
if (!c.includes('plafondDechargeW')) {
|
||||
c = c.replace(ancreCalc, ancreCalc + `
|
||||
|
||||
// ======================================================
|
||||
// CHARGE MAISON AU-DELA DE CE QUE LA BATTERIE SAIT DONNER
|
||||
// ======================================================
|
||||
// Le plafond vient du BMS lui-meme et suit le nombre de modules : rien n'est
|
||||
// code en dur, retirer ou ajouter un module deplace le seuil tout seul.
|
||||
const plafondDechargeA = num(input.battery_max_discharge_a, 0);
|
||||
const tensionParcV = num(input.battery_voltage_v, 0);
|
||||
const plafondDechargeW = plafondDechargeA > 0 && tensionParcV > 0
|
||||
? plafondDechargeA * tensionParcV : 0;
|
||||
// Bande large et volontairement dissymetrique : on ferme a 80 % du plafond, on
|
||||
// ne relache qu'en dessous de 50 %. Les deux seuils portent sur la charge
|
||||
// maison, grandeur que le contacteur ne modifie pas.
|
||||
const seuilFermetureW = plafondDechargeW * 0.80;
|
||||
const seuilRelachementW = plafondDechargeW * 0.50;
|
||||
const chargeAuDessusDuParc = plafondDechargeW > 0 && houseAcPowerW >= seuilFermetureW;
|
||||
const chargeEncoreElevee = plafondDechargeW > 0 && houseAcPowerW >= seuilRelachementW;`);
|
||||
console.log(' calcul : seuils derives du plafond BMS');
|
||||
}
|
||||
|
||||
// ------------------------------------------------------- 3. la fermeture
|
||||
if (!c.includes('turn_on_charge_au_dessus_du_parc')) {
|
||||
const ancreSoc = `} else if (soc <= gridSocOn) {`;
|
||||
if (!c.includes(ancreSoc)) throw new Error('ancre SOC bas introuvable');
|
||||
c = c.replace(ancreSoc, `} else if (chargeAuDessusDuParc) {
|
||||
// Priorite sur la bande SOC : c'est une contrainte de PUISSANCE, elle ne se
|
||||
// resout pas en attendant que le SOC descende.
|
||||
desiredGrid = 'on';
|
||||
desiredLatch = 'on';
|
||||
state = gridIsOn ? 'hold_on_charge_au_dessus_du_parc' : 'turn_on_charge_au_dessus_du_parc';
|
||||
reason.push(
|
||||
\`Charge maison \${Math.round(houseAcPowerW)}W au-dessus de \${Math.round(seuilFermetureW)}W \`
|
||||
+ \`(80% du plafond de decharge BMS \${Math.round(plafondDechargeW)}W) : le reseau prend la difference\`);
|
||||
|
||||
} else if (soc <= gridSocOn) {`);
|
||||
console.log(' calcul : branche de fermeture posee avant la bande SOC');
|
||||
}
|
||||
|
||||
// ------------------------------------------------------- 4. le maintien
|
||||
if (!c.includes('hold_on_charge_encore_elevee')) {
|
||||
const ancreLatch = `} else if (latchIsOn) {`;
|
||||
if (!c.includes(ancreLatch)) throw new Error('ancre latch introuvable');
|
||||
c = c.replace(ancreLatch, `} else if (latchIsOn && chargeEncoreElevee) {
|
||||
// Tant que la maison consomme plus de la moitie de ce que la batterie sait
|
||||
// donner, on garde le reseau — meme si le SOC autoriserait de le lacher.
|
||||
desiredGrid = 'on';
|
||||
desiredLatch = 'on';
|
||||
state = 'hold_on_charge_encore_elevee';
|
||||
reason.push(
|
||||
\`Charge maison \${Math.round(houseAcPowerW)}W encore au-dessus de \${Math.round(seuilRelachementW)}W : on garde le reseau\`);
|
||||
|
||||
} else if (latchIsOn) {`);
|
||||
console.log(' calcul : branche de maintien posee avant le relachement');
|
||||
}
|
||||
|
||||
// ------------------------- 4bis. les coupures immediates cedent le pas
|
||||
// ⚠️ Les trois coupures immediates s'executent AVANT la cascade principale et
|
||||
// posent forcedCut. Sans garde, elles ouvriraient le contacteur que la regle de
|
||||
// charge vient de fermer, et la regle refermerait au cycle suivant :
|
||||
// oscillateur. Elles existent pour relacher le reseau quand l'EVCS n'en a plus
|
||||
// besoin — elles n'ont rien a dire quand c'est la MAISON qui en a besoin.
|
||||
const gardes = [
|
||||
[" userStrategy === 'force_grid_now' &&\n evcsVehicleNoDemandSemantic &&\n latchIsOn\n) {",
|
||||
" userStrategy === 'force_grid_now' &&\n evcsVehicleNoDemandSemantic &&\n latchIsOn &&\n !chargeEncoreElevee // la maison prime\n) {"],
|
||||
[" evcsSurplusState === 'vehicle_not_connected' &&\n soc >= gridSocOff\n) {",
|
||||
" evcsSurplusState === 'vehicle_not_connected' &&\n soc >= gridSocOff &&\n !chargeEncoreElevee // la maison prime\n) {"],
|
||||
[" ) &&\n soc >= gridSocOff\n) {",
|
||||
" ) &&\n soc >= gridSocOff &&\n !chargeEncoreElevee // la maison prime\n) {"],
|
||||
];
|
||||
if (!c.includes('!chargeEncoreElevee')) {
|
||||
for (const [avant, apres] of gardes) {
|
||||
if (!c.includes(avant)) throw new Error('ancre de garde introuvable : ' + avant.slice(0, 40));
|
||||
c = c.replace(avant, apres);
|
||||
}
|
||||
console.log(' calcul : les trois coupures immediates cedent devant la charge maison');
|
||||
}
|
||||
|
||||
// ------------------------------------------------------- 5. tracabilite
|
||||
if (!c.includes('plafond_decharge_w:')) {
|
||||
c = c.replace(' ac_over_safe_limit: acOverSafeLimit,',
|
||||
` ac_over_safe_limit: acOverSafeLimit,
|
||||
plafond_decharge_w: Math.round(plafondDechargeW),
|
||||
charge_au_dessus_du_parc: chargeAuDessusDuParc,`);
|
||||
console.log(' calcul : debug expose');
|
||||
}
|
||||
|
||||
calc.parameters.jsCode = c;
|
||||
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
Reference in New Issue
Block a user