Compare commits

...

2 Commits

Author SHA1 Message Date
Tallagrand Thierry
7b2e8e74d9 Rec 2 : top-off ACM 52,4 -> 52,8 V sur le parc assaini
Le 52,4 V avait ete pose le 06/09 pour menager la cellule 12 faible du pack
ANCIEN (§57). Ce pack est sorti. Cellules du pack 2 a 3-6 mV, la plus haute a
3356 mV, loin des 3550 mV ou le BMS reduit le courant. On releve le top-off
pour atteindre une charge plus pleine et RECALIBRER la jauge SOC Pylontech (qui
derive tant qu'on n'atteint jamais 100 %). 52,8 V = 3,52 V/cellule, sous les
53,2 V autorises par le BMS.

⚠️ Cellules du pack 1 (US5000) pas encore sous surveillance continue (2e cable
en commande). Proprietaire a autorise explicitement malgre le caveat.

patch rejouable tools/patch-20260920-topoff-52v8.mjs. golden-acm 20/20, attendu
acm-jour-topoff mis a jour (52,4->52,8), mutation : retour a 52,4 -> 1 echec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
2026-09-20 13:47:16 +02:00
Tallagrand Thierry
c0704deec3 Recalage de la courbe batterie sur le parc assaini (rec 1)
Re-mesure du 20/09 sur le parc a 13 modules assaini (cellules 3-6 mV) :
tools/mesure-courbe-batterie.mjs donne 0,30 kWh/point PLAT de 25 a 97 %, PLUS DE
GENOU. Le genou a 33 % etait la signature de la cellule 12 morte du pack ancien
(§50), il a disparu avec elle — exactement ce que predisait la note de prudence
du 03/09. La reserve utile a quasiment double (12,4 -> ~23 kWh de 100 a 22 %).

Le scaling modules/7 rattrapait par chance la partie HAUTE (0,169x13/7=0,31 ~
mesure) mais SOUS 33 % le bilan croyait 0,18 kWh/point contre 0,30 reels ->
required_soc surestime en bas d'echelle. Corrige :
- BALANCE.js : kneePct 33->0, pentes 0,169/0,097 -> 0,1615 plat (x13/7 = 0,30).
- INPUT_PARSE : batteryUsableCapacityKwh 2,414 -> 2,308 kWh/module (0,30/13x100).
- helper evcs_balance_usable_capacity_kwh 15 -> 23 (inerte mais coherent).

Tests : bilan 50/50 apres re-figeage des 10 attendus (verifies a la main : ex
rejeu 24/08, 0,82 kWh -> 22 + 0,82/0,1615 = 27,1 % contre 30,5 avant). Mutation
deux sens : ancien genou -> 10 echecs, pente fausse -> 11. Golden controleur
116/116 inchange (capacite -4 %, aucun basculement discret).

⚠️ Courbe a REMESURER apres toute evolution du parc. 2e cable console en
commande pour surveiller aussi les cellules du pack 1 (US5000), aujourd'hui hors
surveillance continue (trames etendues Cerbo mortes, §69).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
2026-09-20 13:45:44 +02:00
8 changed files with 64 additions and 41 deletions

View File

@@ -212,18 +212,22 @@ const modulesEnService = (() => {
})(); })();
const echelleParc = modulesEnService / MODULES_MESURE; const echelleParc = modulesEnService / MODULES_MESURE;
// 20/09/2026 — RE-MESURE sur le parc ASSAINI (13 equivalents US2000 : 4 US5000
// + 5 US2000C, cellules a 3-6 mV). `tools/mesure-courbe-batterie.mjs 8` :
// 0,30 kWh/point PLAT de 25 a 97 %, PLUS DE GENOU. Le genou a 33 % etait bien
// la signature de la cellule 12 morte du pack ancien (§50) — il a disparu avec
// elle, exactement comme le predisait la note de prudence du 03/09. La reserve
// reelle a quasiment double (12,4 -> ~23 kWh utiles de 100 a 22 %).
// Base normalisee a 7 modules (MODULES_MESURE) : 0,30 x 7/13 = 0,1615 par point.
// x echelleParc(13/7) = 0,30 au parc actuel. Pente unique => plus de genou.
// A REMESURER apres toute evolution du parc (2e cable console en commande pour
// surveiller aussi les cellules du pack 1 US5000).
const BATTERY_CURVE = { const BATTERY_CURVE = {
kneePct: 33, kneePct: 0,
kwhPerPointAbove: 0.169 * echelleParc, kwhPerPointAbove: 0.1615 * echelleParc,
kwhPerPointBelow: 0.097 * echelleParc, kwhPerPointBelow: 0.1615 * echelleParc,
}; };
// ⚠️ LE GENOU EST CONSERVE PAR PRUDENCE, PAS PAR MESURE. L'hypothese la mieux
// etayee est qu'il vient des cellules faibles du pack ancien, donc qu'il
// disparait avec lui — auquel cas la pente reste plate et la reserve reelle est
// MEILLEURE qu'annoncee ici. Le garder revient a se tromper du bon cote.
// A trancher en remesurant : `node tools/mesure-courbe-batterie.mjs 8`.
// Energie que la batterie peut delivrer en descendant de socHi a socLo. // Energie que la batterie peut delivrer en descendant de socHi a socLo.
function energyBetweenSoc(socLo, socHi) { function energyBetweenSoc(socLo, socHi) {
if (!Number.isFinite(socLo) || !Number.isFinite(socHi) || socHi <= socLo) return 0; if (!Number.isFinite(socLo) || !Number.isFinite(socHi) || socHi <= socLo) return 0;

View File

@@ -222,12 +222,13 @@ const CFG = {
// ici qu'a des ecarts SOC -> cible, tous deux au-dessus du genou. Si un calcul // ici qu'a des ecarts SOC -> cible, tous deux au-dessus du genou. Si un calcul
// devait un jour traverser le genou, il faut la COURBE (src/energy-balance/ // devait un jour traverser le genou, il faut la COURBE (src/energy-balance/
// BALANCE.js) et non cette constante. // BALANCE.js) et non cette constante.
// 16,9 kWh mesures sur SEPT modules (§41), soit 2,414 kWh par module. Le parc // 20/09/2026 — RE-MESURE sur le parc assaini (13 modules, courbe PLATE a
// ayant change deux fois en une semaine, le nombre de modules se LIT au lieu // 0,30 kWh/point, plus de genou) : 0,30/point / 13 modules x 100 points =
// d'etre code : `input_number.evcs_balance_modules_en_service`, le meme helper // 2,308 kWh par module (etait 2,414 sur l'ancien parc a 7 modules abimes).
// que le bilan energetique — les deux decrivent forcement le meme parc. // Le nombre de modules se LIT (meme helper que le bilan) — les deux decrivent
// Absent ou illisible => 7, donc aucun changement tant que personne ne l'ecrit. // forcement le meme parc. Absent ou illisible => 7 (aucun changement tant que
batteryUsableCapacityKwh: 2.414 * (() => { // personne ne l'ecrit). A remesurer apres toute evolution du parc.
batteryUsableCapacityKwh: 2.308 * (() => {
const n = Number(input.modules_en_service); const n = Number(input.modules_en_service);
return Number.isFinite(n) && n >= 1 && n <= 24 ? n : 7; return Number.isFinite(n) && n >= 1 && n <= 24 ? n : 7;
})(), })(),

View File

@@ -45,7 +45,7 @@ export const balanceScenarios = [
// champ, prouve que l'absence du helper ne change rien. // champ, prouve que l'absence du helper ne change rien.
node: 'BALANCE', node: 'BALANCE',
id: 'bilan-parc-reduit-a-quatre-modules', id: 'bilan-parc-reduit-a-quatre-modules',
label: 'Pack ancien isole : quatre modules au lieu de sept, meme matinee du 24/08 → le SOC exige passe de 30,5 a 35,2 %', label: 'Pack ancien isole : quatre modules au lieu de sept, meme matinee du 24/08 → le SOC exige passe de 27,1 a 30,9 %',
snapshot: { snapshot: {
nowMs: Date.parse('2026-08-24T06:43:00+02:00'), nowMs: Date.parse('2026-08-24T06:43:00+02:00'),
consumption_p50: P50, consumption_p50: P50,
@@ -60,9 +60,9 @@ export const balanceScenarios = [
usable: true, usable: true,
crossingHour: 8, crossingHour: 8,
energyToCoverKwh: 0.82, energyToCoverKwh: 0.82,
requiredSocPct: 35.2, requiredSocPct: 30.9,
gridNeeded: true, gridNeeded: true,
prudentRequiredSocPct: 51.7, prudentRequiredSocPct: 48.2,
deficitTrustworthy: true, deficitTrustworthy: true,
}, },
}, },
@@ -75,7 +75,7 @@ export const balanceScenarios = [
// ensemble, ce que ce scenario verifie en exigeant les deux. // ensemble, ce que ce scenario verifie en exigeant les deux.
node: 'BALANCE', node: 'BALANCE',
id: 'bilan-plancher-manuel-releve-la-cible-et-le-manque', id: 'bilan-plancher-manuel-releve-la-cible-et-le-manque',
label: 'Charge HC manuelle a 85 % : la cible passe de 30,5 a 85 % ET le manque suit (9,95 kWh au lieu de 0,82)', label: 'Charge HC manuelle a 85 % : la cible passe de 27,1 a 85 % ET le manque suit (10,34 kWh au lieu de 0,82)',
snapshot: { snapshot: {
nowMs: Date.parse('2026-08-24T06:43:00+02:00'), nowMs: Date.parse('2026-08-24T06:43:00+02:00'),
consumption_p50: P50, consumption_p50: P50,
@@ -91,7 +91,7 @@ export const balanceScenarios = [
requiredSocPct: 85, requiredSocPct: 85,
gridNeeded: true, gridNeeded: true,
// 21 -> 85 traverse le genou de 33 % : 12 points a 0,097 puis 52 a 0,169. // 21 -> 85 traverse le genou de 33 % : 12 points a 0,097 puis 52 a 0,169.
gridEnergyNeededKwh: 9.95, gridEnergyNeededKwh: 10.34,
// Le plancher passe par `coverage()`, appelee pour les DEUX profils : la // Le plancher passe par `coverage()`, appelee pour les DEUX profils : la
// cible prudente monte aussi. Consequence assumee — pendant une charge // cible prudente monte aussi. Consequence assumee — pendant une charge
// manuelle, P12 cesse de descendre la batterie pour la voiture. // manuelle, P12 cesse de descendre la batterie pour la voiture.
@@ -101,7 +101,7 @@ export const balanceScenarios = [
{ {
node: 'BALANCE', node: 'BALANCE',
id: 'bilan-plancher-manuel-ne-baisse-jamais-la-cible', id: 'bilan-plancher-manuel-ne-baisse-jamais-la-cible',
label: 'CONTREFACTUEL — plancher manuel a 25 %, sous les 30,5 % calcules : le bilan garde SA cible', label: 'CONTREFACTUEL — plancher manuel a 25 %, sous les 27,1 % calcules : le bilan garde SA cible',
snapshot: { snapshot: {
nowMs: Date.parse('2026-08-24T06:43:00+02:00'), nowMs: Date.parse('2026-08-24T06:43:00+02:00'),
consumption_p50: P50, consumption_p50: P50,
@@ -117,8 +117,8 @@ export const balanceScenarios = [
// que RELEVER l'exigence. Un reglage manuel trop bas ne desarme pas le // que RELEVER l'exigence. Un reglage manuel trop bas ne desarme pas le
// bilan, il est simplement sans effet. // bilan, il est simplement sans effet.
usable: true, usable: true,
requiredSocPct: 30.5, requiredSocPct: 27.1,
prudentRequiredSocPct: 41, prudentRequiredSocPct: 37,
}, },
}, },
{ {
@@ -137,14 +137,14 @@ export const balanceScenarios = [
}, },
expect: { expect: {
usable: true, usable: true,
requiredSocPct: 30.5, requiredSocPct: 27.1,
prudentRequiredSocPct: 41, prudentRequiredSocPct: 37,
}, },
}, },
{ {
node: 'BALANCE', node: 'BALANCE',
id: 'bilan-rejeu-24-08-matin', id: 'bilan-rejeu-24-08-matin',
label: 'Rejeu réel du 24/08 06:43 : croisement à 08 h, 0,82 kWh à couvrir → SOC requis 30,5 % (courbe mesurée). Le seuil fixe de 30 % tombait juste ce matin-là — par coïncidence, pas par construction', label: 'Rejeu réel du 24/08 06:43 : croisement à 08 h, 0,82 kWh à couvrir → SOC requis 27,1 % (courbe plate remesuree 20/09). Le seuil fixe de 30 % tombait juste ce matin-là — par coïncidence, pas par construction',
snapshot: { snapshot: {
nowMs: Date.parse('2026-08-24T06:43:00+02:00'), nowMs: Date.parse('2026-08-24T06:43:00+02:00'),
consumption_p50: P50, consumption_p50: P50,
@@ -158,9 +158,9 @@ export const balanceScenarios = [
usable: true, usable: true,
crossingHour: 8, crossingHour: 8,
energyToCoverKwh: 0.82, energyToCoverKwh: 0.82,
requiredSocPct: 30.5, requiredSocPct: 27.1,
gridNeeded: true, gridNeeded: true,
prudentRequiredSocPct: 41, prudentRequiredSocPct: 37,
deficitTrustworthy: true, deficitTrustworthy: true,
}, },
}, },
@@ -180,7 +180,7 @@ export const balanceScenarios = [
expect: { expect: {
usable: true, usable: true,
gridNeeded: false, gridNeeded: false,
requiredSocPct: 30.5, requiredSocPct: 27.1,
}, },
}, },
{ {
@@ -230,8 +230,8 @@ export const balanceScenarios = [
nextDeficitEnergyKwh: 12.77, nextDeficitEnergyKwh: 12.77,
// Au-dessus de 100 : même pleine, la batterie actuelle ne passe pas la // Au-dessus de 100 : même pleine, la batterie actuelle ne passe pas la
// nuit sur le profil prudent. C'est le chiffre du dimensionnement. // nuit sur le profil prudent. C'est le chiffre du dimensionnement.
nextDeficitRequiredSocPct: 102.2, nextDeficitRequiredSocPct: 101.1,
storageShortfallKwh: 0.38, storageShortfallKwh: 0.17,
}, },
}, },
{ {
@@ -277,7 +277,7 @@ export const balanceScenarios = [
// La réserve se calcule quand même : 2,42 kWh pour la matinée du 24/08 // La réserve se calcule quand même : 2,42 kWh pour la matinée du 24/08
// sur le profil prudent, soit SOC 38,1 %. // sur le profil prudent, soit SOC 38,1 %.
stormReserveKwh: 2.42, stormReserveKwh: 2.42,
stormReserveSocPct: 41, stormReserveSocPct: 37,
}, },
}, },
{ {
@@ -321,11 +321,11 @@ export const balanceScenarios = [
stormLevel: 'orange', stormLevel: 'orange',
stormActive: true, stormActive: true,
stormReserveKwh: 2.42, stormReserveKwh: 2.42,
stormReserveSocPct: 41, stormReserveSocPct: 37,
stormCovered: false, stormCovered: false,
// SOC 21 contre 38,1 requis : il manque 2,57 kWh pour tenir la matinée // SOC 21 contre 38,1 requis : il manque 2,57 kWh pour tenir la matinée
// sans réseau. // sans réseau.
stormShortfallKwh: 2.52, stormShortfallKwh: 2.58,
}, },
}, },
{ {
@@ -354,9 +354,9 @@ export const balanceScenarios = [
// 107,1 % — même pleine, la batterie actuelle ne passe pas la nuit sans // 107,1 % — même pleine, la batterie actuelle ne passe pas la nuit sans
// réseau. C'est le signal de dimensionnement, il ne doit pas être écrêté. // réseau. C'est le signal de dimensionnement, il ne doit pas être écrêté.
stormReserveKwh: 12.77, stormReserveKwh: 12.77,
stormReserveSocPct: 102.2, stormReserveSocPct: 101.1,
stormCovered: false, stormCovered: false,
stormShortfallKwh: 1.22, stormShortfallKwh: 0.98,
}, },
}, },
]; ];

View File

@@ -182,10 +182,10 @@ export const scenarios = [
}, },
{ {
id: 'acm-jour-topoff-52v4', id: 'acm-jour-topoff-52v4',
label: "LE CORRECTIF DU 06/09 : SOC 93 dans la tranche 90-95, la tension monte a 52,4 V et le parc reprend de la charge", label: "LE CORRECTIF DU 06/09 : SOC 93 dans la tranche 90-95, la tension monte a 52,8 V (parc assaini, top-off releve 20/09) et le parc reprend de la charge",
time: h(ETE, '15:00'), time: h(ETE, '15:00'),
overrides: { ...parcNeuf, soc_percent: '93', pv_power_w: '4000', battery_voltage_v: '51.1' }, overrides: { ...parcNeuf, soc_percent: '93', pv_power_w: '4000', battery_voltage_v: '51.1' },
expect: { mode: 'day_pv_topoff', profil: 'pv_topoff', courantA: 80, tensionV: 52.4 }, expect: { mode: 'day_pv_topoff', profil: 'pv_topoff', courantA: 80, tensionV: 52.8 },
}, },
{ {
id: 'acm-jour-hold-au-dessus-de-95', id: 'acm-jour-hold-au-dessus-de-95',

View File

@@ -0,0 +1,18 @@
// Rec 2 (20/09/2026) : top-off ACM 52,4 -> 52,8 V, parc assaini.
// Le 52,4 V avait ete pose le 06/09 pour menager la cellule 12 faible du pack
// ANCIEN (§57). Ce pack est sorti ; les cellules du pack 2 sont a 3-6 mV, la
// plus haute a 3356 mV, loin des 3550 mV ou le BMS reduit le courant. On releve
// donc le top-off pour atteindre une charge plus pleine et RECALIBRER la jauge
// SOC Pylontech (qui derive tant qu'on n'atteint jamais 100 %). 52,8 V =
// 3,52 V/cellule, sous les 53,2 V que le BMS autorise.
// ⚠️ Les cellules du pack 1 (US5000) ne sont pas encore sous surveillance
// continue (2e cable en commande) — le proprietaire a autorise explicitement.
import { readFileSync, writeFileSync } from 'node:fs';
const f = 'workflows/victron-auto-charge-manager-v1.2.json';
const s = readFileSync(f, 'utf8');
const avant = "pv_topoff: { current_a: 80, voltage_v: 52.4 }";
const apres = "pv_topoff: { current_a: 80, voltage_v: 52.8 }";
if (!s.includes(avant)) { console.error('motif pv_topoff 52.4 introuvable'); process.exit(1); }
if (s.split(avant).length - 1 !== 1) { console.error('motif non unique'); process.exit(1); }
writeFileSync(f, s.replace(avant, apres));
console.log('top-off 52.4 -> 52.8 V applique');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long