Le bridage a un coupable : cinq cellules du module d'adresse 3
Les exports BatteryView contiennent la sortie brute de la console du BMS : 1798 enregistrements sur 37 jours, avec les 15 tensions de cellule a chaque fois. La preuve etait deja ecrite, il n'y avait rien a attendre de l'apres-midi. Les cellules 2, 6, 10, 12 et 15 montent a 3554-3649 mV quand les dix autres plafonnent a 3440, et descendent a 3128-3236 quand les autres s'arretent a 3252. Monter plus haut ET descendre plus bas est la signature d'une capacite moindre. Le BMS leur attribue d'ailleurs un pourcentage de charge distinct dans chaque enregistrement, et note le module 0 sur 100 contre 100 pour un module sain du meme parc. L'ecart est de 12 mV a 60 % de charge et de 168 mV a 85 % : c'est pourquoi un releve du matin ne peut rien montrer. Deux outils rejouables : la console serie sans dependance, dont le lecteur de table a ete eprouve sur la sortie reelle avant tout branchement, et l'analyse d'historique. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
This commit is contained in:
113
tools/analyse-historique-pylontech.py
Normal file
113
tools/analyse-historique-pylontech.py
Normal file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Analyse les exports d'historique BatteryView d'un module Pylontech.
|
||||
|
||||
BatteryView enregistre la sortie brute de la console du module. Chaque ligne
|
||||
de `datalist history dtls <n>` porte l'etat du module ET les quinze cellules,
|
||||
toutes les trente minutes. Le module en garde 1798, soit trente-sept jours.
|
||||
|
||||
python3 tools/analyse-historique-pylontech.py docs/mesures/batteries/*_history_*.txt
|
||||
|
||||
Ce que l'outil cherche : quelles cellules divergent, de combien, a quel niveau
|
||||
de charge, et si cela s'aggrave. Les tensions sont en millivolts.
|
||||
"""
|
||||
import collections
|
||||
import glob
|
||||
import re
|
||||
import sys
|
||||
|
||||
# Au-dela, le BMS commence a reduire le courant de charge autorise.
|
||||
SEUIL_HAUT_MV = 3550
|
||||
|
||||
|
||||
def lire(chemin):
|
||||
"""Retourne les enregistrements, dedoublonnes par identifiant.
|
||||
|
||||
Les pages de `datalist history dtls` se chevauchent : le meme
|
||||
enregistrement apparait dans plusieurs exports. L'identifiant tranche.
|
||||
"""
|
||||
recs = {}
|
||||
brut = open(chemin, "rb").read()
|
||||
txt = brut.decode("utf-16" if brut[:2] in (b"\xff\xfe", b"\xfe\xff") else "utf-8",
|
||||
errors="replace").replace("\r", "")
|
||||
for ligne in txt.split("\n"):
|
||||
ch = ligne.split()
|
||||
if len(ch) < 18 + 15 * 6:
|
||||
continue
|
||||
if not re.fullmatch(r"\d+", ch[0]) or not re.fullmatch(r"\d\d-\d\d-\d\d", ch[1]):
|
||||
continue
|
||||
recs[int(ch[0])] = dict(
|
||||
id=int(ch[0]), jour=ch[1], heure=ch[2],
|
||||
volt=int(ch[3]), curr=int(ch[4]),
|
||||
etat=ch[10], soc=int(ch[16].rstrip("%")),
|
||||
cellules=[int(ch[18 + k * 6 + 1]) for k in range(15)],
|
||||
)
|
||||
return recs
|
||||
|
||||
|
||||
def main(motifs):
|
||||
recs = {}
|
||||
for m in motifs:
|
||||
for f in sorted(glob.glob(m)):
|
||||
recs.update(lire(f))
|
||||
r = [recs[k] for k in sorted(recs)]
|
||||
if not r:
|
||||
sys.exit("Aucun enregistrement lisible.")
|
||||
|
||||
print(f"{len(r)} enregistrements, du {r[0]['jour']} {r[0]['heure']} "
|
||||
f"au {r[-1]['jour']} {r[-1]['heure']} (heure INTERNE du module)")
|
||||
print()
|
||||
|
||||
print("TENSION EXTREME ATTEINTE PAR CHAQUE CELLULE")
|
||||
hauts = []
|
||||
for i in range(15):
|
||||
vs = [x["cellules"][i] for x in r]
|
||||
marque = ""
|
||||
if max(vs) > SEUIL_HAUT_MV:
|
||||
marque = " <-- monte trop haut ET descend trop bas"
|
||||
hauts.append(i + 1)
|
||||
print(f" cellule {i+1:>2} : max {max(vs)} mV min {min(vs)} mV{marque}")
|
||||
print()
|
||||
if hauts:
|
||||
print(f" Les cellules {', '.join(map(str, hauts))} se distinguent des "
|
||||
f"{15 - len(hauts)} autres.")
|
||||
print(" Monter plus haut en charge ET descendre plus bas en decharge est la")
|
||||
print(" signature d'une capacite moindre : elles se remplissent et se vident")
|
||||
print(" avant les autres. Ce n'est pas un defaut de mesure.")
|
||||
print()
|
||||
|
||||
print("ECART ENTRE CELLULES, EN CHARGE, SELON LE NIVEAU DE CHARGE")
|
||||
print(f" {'SOC':>9} {'releves':>8} {'ecart median':>14} {'ecart max':>11} {'cellule la plus haute':>22}")
|
||||
tr = collections.defaultdict(list)
|
||||
for x in r:
|
||||
if x["etat"] == "Charge":
|
||||
tr[min(90, x["soc"] // 10 * 10)].append(x)
|
||||
for k in sorted(tr):
|
||||
e = sorted(max(x["cellules"]) - min(x["cellules"]) for x in tr[k])
|
||||
vm = max(max(x["cellules"]) for x in tr[k])
|
||||
print(f" {k:>4}-{k+9:<4} {len(e):>8} {e[len(e)//2]:>11} mV {max(e):>8} mV {vm:>19} mV")
|
||||
print()
|
||||
|
||||
print(f"DEPASSEMENTS DE {SEUIL_HAUT_MV} mV, PAR JOUR")
|
||||
j = collections.Counter(x["jour"] for x in r if max(x["cellules"]) > SEUIL_HAUT_MV)
|
||||
if not j:
|
||||
print(" aucun")
|
||||
else:
|
||||
for k in sorted(j):
|
||||
print(f" {k} : {j[k]:>2} releve(s) sur 48")
|
||||
n = sorted(j.values())
|
||||
print(f" total {sum(j.values())} releves sur {len(r)}, repartis sur {len(j)} jours ; "
|
||||
f"median {n[len(n)//2]} par jour")
|
||||
print(" Un nombre stable jour apres jour decrit un desequilibre chronique,")
|
||||
print(" pas une degradation en cours.")
|
||||
print()
|
||||
|
||||
print("LES DIX PIRES MOMENTS")
|
||||
for x in sorted(r, key=lambda z: max(z["cellules"]) - min(z["cellules"]), reverse=True)[:10]:
|
||||
hi, lo = max(x["cellules"]), min(x["cellules"])
|
||||
print(f" {x['jour']} {x['heure']} SOC {x['soc']:>3}% {x['etat']:<7} "
|
||||
f"parc {x['volt']/1000:5.2f} V {x['curr']/1000:>6.2f} A "
|
||||
f"ecart {hi-lo:>3} mV haute = cellule {x['cellules'].index(hi)+1} ({hi} mV)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv[1:] or ["docs/mesures/batteries/*_history_*.txt"])
|
||||
Reference in New Issue
Block a user