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
73 lines
3.6 KiB
JavaScript
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');
|
|
}
|