# -*- coding: utf-8 -*- """Genere le rapport sur l'etat du parc batterie. Rejouable : les mesures vivent dans les constantes en tete, la mise en page en dessous. Mettre a jour une mesure, relancer, republier. """ import pathlib, datetime MAJ = "5 septembre 2026, 14 h" # Amplitude min->max de chaque cellule, relevee sur l'historique du BMS. # C'est LA grandeur qui separe une cellule saine d'une cellule morte : une # cellule qui a perdu sa capacite se remplit et se vide avant ses voisines, # donc elle balaye une plage bien plus large a courant identique. MODULES = [ ("K220803C30160322", "pack ancien · adresse 1", "1681 relevés, 14/08 → 05/09", [ 182,178,178,178,177,548,371,181,179,179,182,691,178,251,178]), ("K220803C30160319", "pack ancien · adresse 3", "1798 relevés, 30/07 → 05/09", [ 184,521,187,186,183,318,185,186,187,473,187,445,186,189,424]), ("K220803C30160324", "isolée (BAT3) · adresse 4", "322 relevés, 23/07 → 05/09", [ 138,124,138,137,139,132,134,142,143,135,136,137,128,135,683]), ] PARC = [ ("K220803C30160322", "ancien", "1", "3", "6 · 7 · 12", 498, 86, "en service"), ("—", "ancien", "2", "0", "—", 0, 0, "en service"), ("K220803C30160319", "ancien", "3", "5", "2 · 6 · 10 · 12 · 15", 416, 179, "en service"), ("K220803C30160324", "ancien", "4", "1", "15", 29, 59, "isolée"), ("Y230202C30371426", "récent", "1", "0", "—", 1, 1, "en service"), ("Y230202C30371425", "récent", "2", "0", "—", 1, 1, "en service"), ("Y230202C30371431", "récent", "3", "0", "—", 1, 1, "en service"), ("Y230202C30371427", "récent", "4", "0", "—", 1, 0, "en service"), ] CHRONO = [ ("13:04:48", "cellule 7 du module …0322 à 3526 mV, cellule 6 du module …0319 à 3541", "sous le seuil"), ("13:05:08", "les mêmes à 3555 et 3575 mV", "niveau 3 franchi"), ("13:05:13", "le BMS abaisse le courant de charge autorisé de 50 à 30 A", "cinq secondes plus tard"), ("13:15:10", "les cellules redescendent à 3499 et 3522 mV, l’alarme repasse au vert", "le plafond, lui, reste à 30 A"), ("13:22:40", "plafond 30 → 40 A", "après 7 min 31 s"), ("13:26:39", "plafond 40 → 35 A, meneuses à 3563 et 3548 mV", "il redéclenche"), ("13:38:10", "plafond 35 → 40 A", "après 11 min 31 s"), ] SEUILS = [(3480, "niveau 1"), (3520, "niveau 2"), (3550, "niveau 3 — le courant se réduit"), (3600, "niveau 4")] def couleur(a): if a >= 600: return "var(--mort)" if a >= 400: return "var(--grave)" if a >= 250: return "var(--attention)" return "var(--sain)" def figure(): """Barres horizontales : amplitude de chaque cellule, par module.""" LARG, LIGNE, GAUCHE, HAUT = 760, 15, 96, 26 ech = 0.86 # px par mV out, y = [], 0 for code, place, _n, amps in MODULES: out.append(f'{code[-4:]}' f'{place}') y += HAUT for i, a in enumerate(amps, 1): larg = a * ech out.append( f'{i}' f'' f'{a}') y += LIGNE y += 14 reperes = [] for mv in (200, 400, 600): x = GAUCHE + mv * ech reperes.append(f'' f'{mv} mV') return (f'' + "".join(reperes) + "".join(out) + '') lignes_parc = "".join( f'{c}{p}' f'{a}{n}{d}' f'{hv}{ov}{e}' for c, p, a, n, d, hv, ov, e in PARC) lignes_chrono = "".join( f'{h}{q}{n}' for h, q, n in CHRONO) lignes_seuils = "".join( f'
  • {mv} mV {nom}
  • ' for mv, nom in SEUILS) HTML = f"""Les cent cinq cellules
    Parc Pylontech · diagnostic en cours

    Les cent cinq cellules

    Huit modules, quinze cellules chacun. Neuf de ces cellules brident la charge des cent cinq autres. Voici lesquelles, comment on le sait, et ce qu’on peut en faire.

    Dernière mise à jour : {MAJ}
    9
    cellules atteintes sur 105
    3
    modules concernés sur 8
    5 s
    entre le franchissement du seuil et la coupure
    30 A
    plafond imposé au parc entier, pour 150 A nominaux

    1Le verdict

    Le parc n’est pas usé. Quatre modules sur huit sont irréprochables : soixante cellules sans une seule anomalie, après plus de trois cents cycles. Un cinquième l’est tout autant.

    Trois modules portent l’ensemble du problème, et à l’intérieur de ces trois, neuf cellules. Quand l’une d’elles atteint 3550 mV, le BMS réduit le courant de charge — non pas de son module, mais de tout le parc. Cinq modules sains sont bridés par neuf cellules.

    {figure()}
    saine décalée gravement atteinte terminale
    Amplitude balayée par chaque cellule entre sa tension minimale et sa tension maximale sur la période d’observation. Une cellule saine suit ses voisines à quelques millivolts près. Une cellule qui a perdu de la capacité se remplit et se vide avant elles, donc elle balaye une plage bien plus large à courant identique — c’est le signe qui ne trompe pas, et il ne demande aucun démontage.

    2Le parc, module par module

    L’identité d’un module est son code-barres, jamais son adresse : chaque chaîne renumérote ses modules à partir de 1, il existe donc deux « module 1 » dans cette installation.

    {lignes_parc}
    Code-barresPackAdr.Cellules atteintes LesquellesSurtensionsSous-tensions

    Le contraste est net : 498 surtensions pour le premier module contre 1 pour chacun des quatre modules du pack récent, qui ont pourtant encaissé dix fois plus de cycles.

    3Le déclenchement, chronométré

    Le 5 septembre, la console a été branchée sur les trois modules du pack ancien pendant que l’alarme montait. Ce qui suit est le relevé, à la seconde.

    {lignes_chrono}
    HeureCe qui se passe

    La découverte la plus utile de la journée tient dans la quatrième ligne : l’alarme repasse au vert bien avant que le plafond ne remonte. Chercher l’alarme dans les journaux fait donc manquer l’essentiel du phénomène — elle dure quelques minutes, le bridage dure des heures.

    Les seuils ne sont pas des valeurs choisies : ce sont ceux que le matériel déclare lui-même.

      {lignes_seuils}

    4Ce qui a été essayé

    La consigne de tension de charge a été abaissée de 51,3 à 51,0 V pour voir si le contournement suffisait à ne plus jamais déclencher.

    Il ne suffit pas. À 92 % de charge, les cellules meneuses ont atteint 3563 et 3548 mV malgré la consigne abaissée, et le plafond a recommencé à osciller entre 35 et 40 A. Le système ne reste pas « bloqué » à une valeur : il cherche en permanence le courant maximal que la cellule la plus faible tolère.

    Une mesure explique pourquoi la tension seule ne peut pas régler la question. À tension de parc identique, la cellule meneuse était à 3555 mV sous 50 A et à 3496 mV sous 30 A. Son excursion est donc largement produite par sa résistance interne multipliée par le courant, et pas seulement par son état de charge.

    5D’où viennent les dégâts

    Pas de l’usure. Le pack récent a encaissé trois cent quarante cycles en un an et ses soixante cellules sont intactes : cycler n’abîme pas ces modules.

    Les deux cellules terminales du parc sont descendues à 2953 et 2939 mV, sous le seuil d’alarme basse. Un accumulateur au lithium-fer-phosphate mené là sous charge perd de la capacité de façon irréversible. Ces cellules n’ont pas vieilli, elles ont été vidées trop bas.

    Et il y a une boucle à rompre. La jauge d’état de charge du parc suit la cellule la plus faible. Quand le système croit toucher son plancher de sécurité à 22 %, cette cellule est déjà bien plus bas que le chiffre ne le laisse croire. Le plancher est exprimé dans une grandeur que la cellule endommagée fausse elle-même : il ne protège pas ce qu’il croit protéger, et rien n’empêche le même mécanisme d’atteindre le pack sain.

    6Ce qui vient

    Une alimentation stabilisée permettra de reprendre le rééquilibrage au banc, en commençant par le module isolé : il n’a qu’une cellule à traiter, il est hors service, et rien de ce qu’on y tentera ne peut nuire au parc.

    Une limite à garder en tête : un rééquilibrage corrige un décalage de charge, pas une perte de capacité. Les cellules modérément décalées sont de bonnes candidates. Les deux cellules terminales le sont beaucoup moins — et c’est précisément ce que l’essai sur le module isolé dira, sans risque.

    """ p = pathlib.Path("docs/pages/rapport-batteries.html") p.parent.mkdir(parents=True, exist_ok=True) p.write_text(HTML, encoding="utf-8") print(f"{p} {len(HTML)} caracteres")