diff --git a/tools/pylontech-console.mjs b/tools/pylontech-console.mjs index 63895d3..164a2c5 100644 --- a/tools/pylontech-console.mjs +++ b/tools/pylontech-console.mjs @@ -332,6 +332,43 @@ async function main() { return; } + // --------------------------------------------------------------------- + // COURANT PAR MODULE — un seul `pwr` pour toute la chaine + // --------------------------------------------------------------------- + // `soh ` donne les quinze tensions mais AUCUN courant : c'est pourquoi la + // colonne courant_mA du CSV est restee vide depuis la mise en service. + // `pwr` donne courant, tension et SOC pour TOUS les modules en un appel — + // une commande serie par cycle au lieu d'une par module. + // + // ⚠️ LE TABLEAU N'A PAS LES MEMES COLONNES SELON LA FAMILLE DE MODULES. + // Mesure du 16/09 : les US5000 (firmware B69.26) intercalent Tlow.Id, + // Thigh.Id, Vlow.Id et Vhigh.Id que les US2000C (B69.13) n'ont pas. Un + // parseur a index figes lit donc n'importe quoi des qu'on change de pack. + // On resout les colonnes PAR LEUR NOM, dans l'en-tete. + // + // Les colonnes utiles (Volt, Curr, Coulomb) precedent toutes `Time`, dont la + // valeur contient une espace — le decoupage positionnel reste donc valable. + function courantsParModule(txt) { + const lignes = txt.split('\n').map((l) => l.replace(/\r/g, '').trimEnd()); + const iEntete = lignes.findIndex((l) => /^\s*Power\s+Volt\s+Curr\b/.test(l)); + if (iEntete === -1) return new Map(); + const cols = lignes[iEntete].trim().split(/\s+/); + const iCurr = cols.indexOf('Curr'); + const iVolt = cols.indexOf('Volt'); + const iSoc = cols.indexOf('Coulomb'); + if (iCurr === -1) return new Map(); + const m = new Map(); + for (const l of lignes.slice(iEntete + 1)) { + const c = l.trim().split(/\s+/); + const ad = parseInt(c[0], 10); + if (!Number.isFinite(ad) || c[iCurr] === undefined || c[iCurr] === '-') continue; + const mA = parseInt(c[iCurr], 10); + if (!Number.isFinite(mA)) continue; + m.set(ad, { mA, mV: parseInt(c[iVolt], 10) || null, soc: iSoc > -1 ? c[iSoc] : null }); + } + return m; + } + if (arg('suivi')) { const pas = Math.max(10, +(arg('suivi', '30')) || 30); const nom = `${horodateFichier()}-suivi.csv`; @@ -370,6 +407,12 @@ async function main() { console.log('Ctrl-C pour arreter.\n'); for (;;) { if (adresses.length) { + // Un `pwr` pour toute la chaine, avant la boucle par module. Si la + // lecture echoue on continue sans courant plutot que de perdre les + // tensions, qui sont l'essentiel du releve. + let puis = new Map(); + try { puis = courantsParModule(await commande(l, 'pwr', 10000)); } + catch { /* on garde les tensions */ } for (const ad of adresses) { const t = await commande(l, `soh ${ad}`, 10000); // `soh` : " " @@ -381,9 +424,12 @@ async function main() { if (c.length < 10) { console.log(`${horodate()} module ${ad} : illisible`); continue; } const v = c.map((x) => x.mV); const hi = Math.max(...v), lo = Math.min(...v); - fs.appendFileSync(f, [new Date().toISOString(), codes.get(ad), '', ...v, hi - lo].join(',') + '\n'); + const p = puis.get(ad); + fs.appendFileSync(f, [new Date().toISOString(), codes.get(ad), + p ? p.mA : '', ...v, hi - lo].join(',') + '\n'); console.log(`${horodate()} ${codes.get(ad).slice(-4)} haute ${hi} mV (cellule ${c.find((x) => x.mV === hi).n})` + ` basse ${lo} mV ecart ${String(hi - lo).padStart(3)} mV` + + (p ? ` ${(p.mA / 1000).toFixed(1)} A ${p.soc || ''}` : '') + (hi >= 3600 ? ' NIVEAU 4' : hi >= 3550 ? ' NIVEAU 3, le courant se reduit' : '')); } await new Promise((r) => setTimeout(r, pas * 1000));