Files
EVCS/tools/mesure-courbe-batterie.mjs
Tallagrand Thierry 6603c5dfd2 La conversion energie/SOC devient une courbe mesuree, pas une constante
576 paliers de SOC mesures sur 8 jours (tools/mesure-courbe-batterie.mjs,
ajoute) : pour chaque palier ou le SOC reste a une valeur puis descend d un
point, on integre la puissance batterie, en ecartant les paliers ou la batterie
a charge et ceux ou le reseau a apporte quoi que ce soit.

Resultat : plate a 0,169 kWh/point de 33 a 93 — six tranches independantes entre
0,163 et 0,171 — genou net a 32-33, puis 0,097 en dessous. Sous le genou un
point de SOC vaut 57 % de ce qu il vaut au-dessus, ce qui est coherent avec le
parc reel (un module en panne sur huit, un autre qui faiblit).

Energie utile totale de 100 a 22 % : 12,39 kWh mesures, contre 11,70 vus par le
bilan (15 kWh a plat) et 13,65 par INPUT_PARSE (17,5). Les deux constantes
etaient fausses, dans des sens opposes.

Comme 15 kWh se situe ENTRE les deux pentes, l erreur changeait de signe : SOC
requis trop bas de 3 a 4 points pour un petit deficit, trop haut de 4 a 6 pour
un gros. Cela corrige au passage une affirmation inexacte de la section 40, ou
j avais ecrit que required_soc etait systematiquement trop bas d un facteur
1,75 : ce facteur comparait la vitesse de descente du SOC a celle de la ligne,
deux grandeurs differentes, et l ecart venait surtout de la sous-estimation de
la consommation par le profil p50. La correction est posee en tete de la
section 40.

Trois fonctions remplacent les cinq conversions lineaires de BALANCE.js ;
requiredSocAtStartPct reste non ecrete au-dela de 100, l extrapolation utilisant
la pente haute. INPUT_PARSE passe de 17,5 a 16,9 avec la consigne explicite
qu un calcul traversant le genou doit lire la courbe et non cette constante.

Tests du bilan 42/42 apres mise a jour deliberee de six attendus, chacun verifie
a la main contre la courbe. Un libelle corrige : le seuil fixe de 30 % tombait
juste ce matin-la par coincidence, pas par construction. Deux mutations — pente
basse ramenee a la pente haute, puis genou ramene au plancher — font tomber les
six memes scenarios. Golden 78, hysteresis 7 et advisor inchanges.

Les trois nombres sont des resultats de mesure, pas des reglages, et vivent dans
le code avec l outil qui les produit ; a rejouer apres l extension du parc.

Principale erreur restante du bilan, desormais devant la conversion : le profil
de consommation p50 sous-estimait de 33 % les heures 03-06 de la nuit du
02-03/09. A instruire separement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
2026-09-03 23:28:57 +02:00

73 lines
3.6 KiB
JavaScript

// Energie delivree par POINT de SOC, mesuree point par point.
// Methode : pour chaque palier ou le SOC reste a la valeur v puis DESCEND a
// v-1, on integre la puissance batterie sur ce palier. Uniquement si la
// batterie decharge tout du long et si le reseau n'apporte rien (sinon la
// mesure porte sur autre chose que la batterie).
import fs from 'node:fs';
const ROOT='/mnt/projects/EVCS/EVCS';
function token(){for(const l of fs.readFileSync(ROOT+'/.env','utf8').split('\n')){const m=l.match(/^\s*HA_TOKEN\s*=\s*(.+?)\s*$/);if(m)return m[1].replace(/^["']|["']$/g,'');}throw 0;}
const HA='http://10.10.10.17:8123';const H={Authorization:'Bearer '+token()};
async function hist(ent,t0,t1){
const u=`${HA}/api/history/period/${t0.toISOString()}?end_time=${t1.toISOString()}&filter_entity_id=${ent}&minimal_response&significant_changes_only=0`;
const d=await (await fetch(u,{headers:H})).json();
return (d[0]||[]).map(s=>({t:new Date(s.last_changed||s.last_updated).getTime(),v:Number(s.state)})).filter(p=>Number.isFinite(p.v));
}
const DAYS=Number(process.argv[2]||8);
const end=new Date(process.argv[3]||Date.now());
const bins={}; // soc -> {kwh, count, secs}
let paliers=0, rejetes=0;
for(let d=0; d<DAYS; d++){
const t1=new Date(end.getTime()-d*86400000);
const t0=new Date(t1.getTime()-86400000);
const [soc,bp,grid]=await Promise.all([
hist('sensor.victron_battery_soc',t0,t1),
hist('sensor.victron_battery_power',t0,t1),
hist('sensor.victron_grid_power_l1',t0,t1),
]);
if(soc.length<5||bp.length<5) continue;
const at=(arr,t)=>{let v=null;for(const p of arr){if(p.t<=t)v=p.v;else break;}return v;};
// paliers de SOC
for(let i=0;i<soc.length-1;i++){
const v=soc[i].v, vn=soc[i+1].v;
if(vn !== v-1) continue; // on ne garde que les descentes d'un point
const a=soc[i].t, b=soc[i+1].t;
if(b-a < 60000 || b-a > 4*3600000) { rejetes++; continue; }
// integration de la puissance batterie sur [a,b], et controle reseau
const pts=bp.filter(p=>p.t>=a&&p.t<=b);
const first=at(bp,a);
if(first===null) { rejetes++; continue; }
const seq=[{t:a,v:first},...pts,{t:b,v:pts.length?pts[pts.length-1].v:first}];
let kwh=0, positif=false;
for(let k=0;k<seq.length-1;k++){
const dt=(seq[k+1].t-seq[k].t)/3600000;
if(dt<=0) continue;
if(seq[k].v>50) positif=true; // la batterie s'est mise a charger
kwh += -seq[k].v*dt/1000;
}
if(positif||kwh<=0){ rejetes++; continue; }
// reseau : on rejette si import moyen significatif
const gpts=grid.filter(p=>p.t>=a&&p.t<=b);
const g0=at(grid,a);
const gseq=[{t:a,v:g0===null?0:g0},...gpts];
let gkwh=0;
for(let k=0;k<gseq.length-1;k++){const dt=(gseq[k+1].t-gseq[k].t)/3600000;if(dt>0)gkwh+=gseq[k].v*dt/1000;}
if(gkwh>0.02){ rejetes++; continue; }
const key=Math.round(v);
bins[key]=bins[key]||{kwh:0,n:0};
bins[key].kwh+=kwh; bins[key].n++;
paliers++;
}
}
console.log(`paliers retenus: ${paliers} | rejetes: ${rejetes}`);
const keys=Object.keys(bins).map(Number).sort((a,b)=>a-b);
console.log('\nSOC n kWh/point');
for(const k of keys) console.log(String(k).padStart(3), String(bins[k].n).padStart(3), (bins[k].kwh/bins[k].n).toFixed(3).padStart(7));
// regroupement par tranches de 10
console.log('\ntranche paliers kWh/point capacite implicite');
for(let lo=15; lo<100; lo+=10){
const ks=keys.filter(k=>k>=lo&&k<lo+10);
const n=ks.reduce((s,k)=>s+bins[k].n,0), e=ks.reduce((s,k)=>s+bins[k].kwh,0);
if(!n) continue;
console.log(`${lo}-${lo+9}`.padEnd(9), String(n).padStart(7), (e/n).toFixed(3).padStart(11), (100*e/n).toFixed(1).padStart(18),'kWh');
}