feat(ups): varsta acumulatorilor devine criteriu in testul lunar

Utilizatorul a confirmat ca acumulatorii au fost schimbati pe 2024-01-09 (data
aproximativa). Asta scoate la iveala o limita a evaluarii pe tendinta: NUT a fost
instalat pe 2025-10-06, cand acumulatorii aveau deja ~21 luni, deci linia de baza
NU e o baterie noua. Raportul de 1.16x masoara degradarea peste o baterie care
isi pierduse deja o parte din capacitate. Verdictul EXCELLENT e corect ca
tendinta, dar nu inseamna "ca noua".

Varsta devine criteriu separat, citit din /etc/nut/battery-install-date:
sub 3 ani niciun efect, 3-4 ani nota vizibila in raport, peste 4 ani forteaza cel
putin FAIR indiferent de tendinta. Fara fisier, scriptul merge normal si cere
completarea lui.

Verificat pe pvemini cu testul de baterie anulat, pe trei date: 32 luni (reala)
-> EXCELLENT, 39 luni -> EXCELLENT cu nota, 56 luni -> FAIR fortat. Fallback-ul
fara fisier intoarce varsta goala, nu eroare.

Un bug prins la verificare: extragerea datei lua prima potrivire din fisier, care
era un an dintr-un comentariu, si dadea varsta 0. Ambele extrageri sar acum peste
liniile care incep cu #.

Prag de 3 ani: 2027-01-09. Prag de 4 ani, cand testul incepe sa avertizeze
singur: 2028-01-09.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RRyaDj39hPQ89SZS6URRpS
This commit is contained in:
Marius
2026-08-27 15:00:16 +03:00
parent 0b8e8f6892
commit 83bb8b8d8c
4 changed files with 134 additions and 5 deletions

View File

@@ -0,0 +1,15 @@
# Data instalarii acumulatorilor din UPS, format YYYY-MM-DD, o singura data.
# Se instaleaza pe pvemini in /etc/nut/battery-install-date
#
# Citit de /opt/scripts/ups-monthly-test.sh ca sa raporteze varsta si sa coboare
# verdictul la FAIR peste 4 ani, indiferent de tendinta. Motivul: linia de baza a
# tendintei a fost masurata in oct 2025, cand acumulatorii aveau deja ~21 luni,
# deci raportul nu se compara cu o baterie noua.
#
# LA INLOCUIRE: actualizeaza data AICI si pe server, si reseteaza linia de baza
# (muta /var/log/ups-battery-trend.csv). Vezi UPS-BATTERY-REPLACEMENT.md.
#
# Acumulatori: 2x 12V 7Ah in serie (24V nominal).
# Sursa datei: confirmata de utilizator 2026-08-27, aproximativa ("cred ca").
2024-01-09

View File

@@ -22,6 +22,21 @@ bateriei, nu pe praguri absolute de încărcare (vezi
| **Health: EXCELLENT** (< 1.25x) | ✅ Baterie OK | | **Health: EXCELLENT** (< 1.25x) | ✅ Baterie OK |
| **Health: BASELINE** | Se colectează istoric, sub 9 rulări | | **Health: BASELINE** | Se colectează istoric, sub 9 rulări |
### Istoricul acestui UPS
| Ce | Când | Sursă |
|----|------|-------|
| Instalare acumulatori actuali | **2024-01-09** | confirmată de utilizator 2026-08-27, aproximativă |
| Instalare NUT / început monitorizare | 2025-10-06 | jurnal |
| Linia de bază a tendinței | oct–dec 2025 | acumulatori deja de ~21 luni |
Acumulatori: **2x 12V 7Ah în serie** (24V nominal). Data se ține în
`/etc/nut/battery-install-date`, versionată în
`proxmox/cluster/ups/config/battery-install-date`.
**Prag de 3 ani: 2027-01-09.** La 4 ani (2028-01-09) testul lunar coboară automat
verdictul la FAIR și începe să trimită avertismente, indiferent de tendință.
### Alte Semne: ### Alte Semne:
- Vârsta bateriei > 3-4 ani - Vârsta bateriei > 3-4 ani
@@ -356,8 +371,16 @@ mv /var/log/ups-battery-trend.csv /var/log/ups-battery-trend.csv.$(date +%Y%m%d)
# primele 9 rulări vor raporta BASELINE, apoi verdictul redevine valid # primele 9 rulări vor raporta BASELINE, apoi verdictul redevine valid
``` ```
Notează data înlocuirii în acest document — jurnalul nu o conține, iar vârsta **Actualizează și data instalării**, în ambele locuri:
bateriei nu se poate deduce din software.
```bash
# pe pvemini
echo "$(date +%Y-%m-%d)" > /etc/nut/battery-install-date
# în repo: proxmox/cluster/ups/config/battery-install-date, plus tabelul de mai sus
```
Fără asta, testul lunar continuă să raporteze vârsta bateriei vechi și va coborî
verdictul la FAIR pe criteriu de vârstă deși bateria e nouă.
- Voltage drop: 1-2V - Voltage drop: 1-2V
- Recuperare rapidă: < 60 secunde - Recuperare rapidă: < 60 secunde

View File

@@ -95,6 +95,25 @@ Se poate reconstrui oricând din jurnal cu
14 rulări extrase din `/var/log/ups-monthly-test.log` (2025-10-06 → 2026-08-01), 14 rulări extrase din `/var/log/ups-monthly-test.log` (2025-10-06 → 2026-08-01),
ca linia de bază să fie validă imediat și nu peste 9 luni. ca linia de bază să fie validă imediat și nu peste 9 luni.
### Vârsta acumulatorilor
Vârsta e al doilea criteriu, independent de tendință, pentru că **linia de bază nu
a fost măsurată pe o baterie nouă** — vezi avertismentul de mai jos.
Data instalării se ține în `/etc/nut/battery-install-date` (o linie, `YYYY-MM-DD`,
comentariile cu `#` se ignoră). Versionată în
`proxmox/cluster/ups/config/battery-install-date`.
| Vârstă | Efect asupra verdictului |
|--------|--------------------------|
| < 3 ani | niciunul |
| 3 – 4 ani | notă vizibilă în raport, verdictul rămâne pe tendință |
| > 4 ani | forțează cel puțin **FAIR**, indiferent de tendință |
| necunoscută | notă care cere completarea fișierului |
Dacă fișierul lipsește sau nu conține o dată validă, scriptul funcționează normal
și raportează vârsta ca necunoscută.
### Starea la 2026-08-27 ### Starea la 2026-08-27
| Perioadă | Cădere de tensiune | | Perioadă | Cădere de tensiune |
@@ -105,6 +124,18 @@ ca linia de bază să fie validă imediat și nu peste 9 luni.
Raport **1.16x** → **EXCELLENT**. Creșterea de ~16% în 10 luni e reală, dar sub Raport **1.16x** → **EXCELLENT**. Creșterea de ~16% în 10 luni e reală, dar sub
banda de avertizare. Alarma (FAIR) s-ar da la o cădere de ~2.73 V. banda de avertizare. Alarma (FAIR) s-ar da la o cădere de ~2.73 V.
Acumulatori instalați **2024-01-09** (dată aproximativă, confirmată de utilizator),
deci **32 de luni** la 2026-08-27. Pragul de 3 ani vine pe 2027-01-09.
> ⚠️ **Linia de bază nu e o baterie nouă.** NUT a fost instalat pe 2025-10-06, când
> acumulatorii aveau deja ~21 de luni. Raportul de 1.16x măsoară deci degradarea
> *peste* o baterie care își pierduse deja o parte din capacitate — nu față de
> starea din fabrică. Verdictul EXCELLENT e corect ca tendință, dar nu înseamnă
> „baterie ca nouă". De aceea vârsta e criteriu separat.
>
> **La următoarea înlocuire linia de bază se resetează**, și abia atunci raportul
> va porni de la o baterie chiar nouă.
### De ce s-a schimbat metoda ### De ce s-a schimbat metoda
`battery.charge` **nu e o măsurătoare independentă** la acest UPS. Driverul `battery.charge` **nu e o măsurătoare independentă** la acest UPS. Driverul

View File

@@ -13,6 +13,7 @@
LOGFILE="/var/log/ups-monthly-test.log" LOGFILE="/var/log/ups-monthly-test.log"
TRENDFILE="/var/log/ups-battery-trend.csv" TRENDFILE="/var/log/ups-battery-trend.csv"
BATTERY_DATE_FILE="/etc/nut/battery-install-date"
UPS_NAME="nutdev1" UPS_NAME="nutdev1"
UPS_USER="admin" UPS_USER="admin"
UPS_PASS="parola99" UPS_PASS="parola99"
@@ -33,6 +34,21 @@ log() {
BASELINE_RUNS=6 BASELINE_RUNS=6
TREND_WINDOW=3 TREND_WINDOW=3
# Vârsta acumulatorilor, în luni, din $BATTERY_DATE_FILE (o singură linie,
# format YYYY-MM-DD). Contează separat de tendință: linia de bază se poate să fi
# fost măsurată pe o baterie deja îmbătrânită, caz în care raportul arată bine
# fără ca bateria să fie bună. Vârsta e singurul reper absolut pe care îl avem.
battery_age_months() {
[ -r "$BATTERY_DATE_FILE" ] || return 0
local d
d=$(grep -v '^[[:space:]]*#' "$BATTERY_DATE_FILE" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' | head -1)
[ -n "$d" ] || return 0
local t0 t1
t0=$(date -d "$d" +%s 2>/dev/null) || return 0
t1=$(date +%s)
awk -v a="$t0" -v b="$t1" 'BEGIN { printf "%.0f", (b - a) / 2629746 }'
}
# Adaugă rularea curentă în fișierul de tendință # Adaugă rularea curentă în fișierul de tendință
append_trend() { append_trend() {
if [ ! -f "$TRENDFILE" ]; then if [ ! -f "$TRENDFILE" ]; then
@@ -90,6 +106,7 @@ Battery Voltage Drop: {{ voltage_drop }}V
Baseline Voltage Drop: {{ baseline_drop }}V ({{ baseline_runs }} rulari de referinta) Baseline Voltage Drop: {{ baseline_drop }}V ({{ baseline_runs }} rulari de referinta)
Recent Median (3 run): {{ recent_drop }}V Recent Median (3 run): {{ recent_drop }}V
Trend vs Baseline: {{ trend_ratio }}x Trend vs Baseline: {{ trend_ratio }}x
Battery Age: {{ battery_age }} luni (instalati {{ battery_install }})
Minimum Charge Reached: {{ min_charge }}% Minimum Charge Reached: {{ min_charge }}%
Minimum Voltage: {{ min_voltage }}V Minimum Voltage: {{ min_voltage }}V
Recovery Time: {{ recovery_time }}s Recovery Time: {{ recovery_time }}s
@@ -183,6 +200,10 @@ EOFTEMPLATE
<div class="metric-label">Baseline Drop</div> <div class="metric-label">Baseline Drop</div>
<div class="metric-value">{{ baseline_drop }}V</div> <div class="metric-value">{{ baseline_drop }}V</div>
</div> </div>
<div class="metric">
<div class="metric-label">Battery Age</div>
<div class="metric-value">{{ battery_age }} luni</div>
</div>
</div> </div>
<div class="section"> <div class="section">
@@ -233,7 +254,7 @@ EOFTEMPLATE
# Template-urile se rescriu și dacă sunt dintr-o versiune veche, fără câmpurile # Template-urile se rescriu și dacă sunt dintr-o versiune veche, fără câmpurile
# de tendință — altfel raportul ar ieși cu metrici lipsă. # de tendință — altfel raportul ar ieși cu metrici lipsă.
if [ ! -f "$TEMPLATE_DIR/ups-battery-test-subject.txt.hbs" ] || \ if [ ! -f "$TEMPLATE_DIR/ups-battery-test-subject.txt.hbs" ] || \
! grep -q 'trend_ratio' "$TEMPLATE_DIR/ups-battery-test-body.txt.hbs" 2>/dev/null; then ! grep -q 'battery_age' "$TEMPLATE_DIR/ups-battery-test-body.txt.hbs" 2>/dev/null; then
log "Creating/updating notification templates..." log "Creating/updating notification templates..."
create_templates create_templates
fi fi
@@ -366,6 +387,21 @@ log " Scădere tensiune: $VOLTAGE_DROP V (de la $BEFORE_VOLTAGE V la $MIN_VOLTA
append_trend "$VOLTAGE_DROP" "$MIN_VOLTAGE" "$CHARGE_DROP" "$BEFORE_LOAD" "$AFTER_LOAD" "$TEST_DURATION" append_trend "$VOLTAGE_DROP" "$MIN_VOLTAGE" "$CHARGE_DROP" "$BEFORE_LOAD" "$AFTER_LOAD" "$TEST_DURATION"
BATTERY_AGE=$(battery_age_months)
BATTERY_INSTALL=$(grep -v '^[[:space:]]*#' "$BATTERY_DATE_FILE" 2>/dev/null | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' | head -1)
# 0 = sub 3 ani, 1 = 3-4 ani (doar notă), 2 = peste 4 ani (forțează FAIR).
# VRLA de 7Ah coboară sub 50% capacitate tipic la 3-4 ani, iar tendința nu prinde
# asta dacă linia de bază a fost măsurată pe o baterie deja uzată.
AGE_CLASS=0
if [ -n "$BATTERY_AGE" ]; then
if [ "$BATTERY_AGE" -ge 48 ]; then
AGE_CLASS=2
elif [ "$BATTERY_AGE" -ge 36 ]; then
AGE_CLASS=1
fi
fi
TREND_ROWS=$(trend_rows) TREND_ROWS=$(trend_rows)
BASELINE_DROP=$(trend_median head $BASELINE_RUNS) BASELINE_DROP=$(trend_median head $BASELINE_RUNS)
RECENT_DROP=$(trend_median tail $TREND_WINDOW) RECENT_DROP=$(trend_median tail $TREND_WINDOW)
@@ -386,6 +422,11 @@ log " Rulări înregistrate: $TREND_ROWS (fișier: $TRENDFILE)"
log " Linie de bază (mediana primelor $BASELINE_RUNS_USED): ${BASELINE_DROP:-n/a} V" log " Linie de bază (mediana primelor $BASELINE_RUNS_USED): ${BASELINE_DROP:-n/a} V"
log " Recent (mediana ultimelor $TREND_WINDOW): ${RECENT_DROP:-n/a} V" log " Recent (mediana ultimelor $TREND_WINDOW): ${RECENT_DROP:-n/a} V"
log " Raport: ${TREND_RATIO}x" log " Raport: ${TREND_RATIO}x"
if [ -n "$BATTERY_AGE" ]; then
log " Vârstă acumulatori: $BATTERY_AGE luni (instalați $BATTERY_INSTALL)"
else
log " Vârstă acumulatori: necunoscută ($BATTERY_DATE_FILE lipsește)"
fi
BATTERY_HEALTH="UNKNOWN" BATTERY_HEALTH="UNKNOWN"
HEALTH_CLASS="fair" HEALTH_CLASS="fair"
@@ -393,6 +434,15 @@ HEALTH_EMOJI="[INFO]"
HEALTH_DESCRIPTION="" HEALTH_DESCRIPTION=""
RECOMMENDATIONS="" RECOMMENDATIONS=""
AGE_NOTE=""
case "$AGE_CLASS" in
1) AGE_NOTE="<li>📅 Acumulatorii au $BATTERY_AGE luni (instalați $BATTERY_INSTALL). VRLA de 7Ah intră în zona de uzură la 3-4 ani — bugetează înlocuirea.</li>" ;;
2) AGE_NOTE="<li>⚠️ Acumulatorii au $BATTERY_AGE luni (instalați $BATTERY_INSTALL), <strong>peste 4 ani</strong>. Verdictul e coborât la FAIR pe criteriu de vârstă, indiferent de tendință.</li>" ;;
esac
if [ -z "$BATTERY_AGE" ]; then
AGE_NOTE="<li>Vârsta acumulatorilor e necunoscută — scrie data instalării în $BATTERY_DATE_FILE (YYYY-MM-DD).</li>"
fi
# Praguri de tensiune absolută, ca plasă de siguranță. Nu înlocuiesc tendința — # Praguri de tensiune absolută, ca plasă de siguranță. Nu înlocuiesc tendința —
# prind cazul în care bateria cedează brusc, înainte să apuce media să urce. # prind cazul în care bateria cedează brusc, înainte să apuce media să urce.
FLOOR=$(awk -v v="$MIN_VOLTAGE" 'BEGIN { FLOOR=$(awk -v v="$MIN_VOLTAGE" 'BEGIN {
@@ -402,7 +452,7 @@ FLOOR=$(awk -v v="$MIN_VOLTAGE" 'BEGIN {
else { print "none" } else { print "none" }
}') }')
if [ "$TREND_ROWS" -lt $((BASELINE_RUNS + TREND_WINDOW)) ] && [ "$FLOOR" = "none" ]; then if [ "$TREND_ROWS" -lt $((BASELINE_RUNS + TREND_WINDOW)) ] && [ "$FLOOR" = "none" ] && [ "$AGE_CLASS" -lt 2 ]; then
BATTERY_HEALTH="BASELINE" BATTERY_HEALTH="BASELINE"
HEALTH_CLASS="good" HEALTH_CLASS="good"
HEALTH_EMOJI="[INFO]" HEALTH_EMOJI="[INFO]"
@@ -424,9 +474,10 @@ elif [ "$TREND_RATIO" = "n/a" ]; then
RECOMMENDATIONS="<ul><li>Verifică $TRENDFILE</li><li>Verifică dacă upsc întoarce battery.voltage</li></ul>" RECOMMENDATIONS="<ul><li>Verifică $TRENDFILE</li><li>Verifică dacă upsc întoarce battery.voltage</li></ul>"
log " Sănătate baterie: NECUNOSCUTĂ (tendință incalculabilă)" log " Sănătate baterie: NECUNOSCUTĂ (tendință incalculabilă)"
else else
RATIO_CLASS=$(awk -v r="$TREND_RATIO" -v f="$FLOOR" 'BEGIN { RATIO_CLASS=$(awk -v r="$TREND_RATIO" -v f="$FLOOR" -v a="$AGE_CLASS" 'BEGIN {
c = (r < 1.25) ? 1 : (r < 1.50) ? 2 : (r < 2.00) ? 3 : 4 c = (r < 1.25) ? 1 : (r < 1.50) ? 2 : (r < 2.00) ? 3 : 4
if (f == "warn" && c < 3) c = 3 if (f == "warn" && c < 3) c = 3
if (a == 2 && c < 3) c = 3
print c print c
}') }')
case "$RATIO_CLASS" in case "$RATIO_CLASS" in
@@ -465,6 +516,12 @@ else
esac esac
fi fi
# Nota de vârstă se lipește la finalul listei de recomandări, oricare ar fi
# verdictul — inclusiv pe EXCELLENT, unde altfel s-ar pierde.
if [ -n "$AGE_NOTE" ]; then
RECOMMENDATIONS=$(echo "$RECOMMENDATIONS" | sed "s#</ul>\$#${AGE_NOTE}</ul>#")
fi
# 7. Monitorizare recuperare (30 secunde) # 7. Monitorizare recuperare (30 secunde)
log "" log ""
log "Step 6: Monitorizare recuperare baterie..." log "Step 6: Monitorizare recuperare baterie..."
@@ -515,6 +572,8 @@ my \$template_data = {
'recent_drop' => '${RECENT_DROP:-n/a}', 'recent_drop' => '${RECENT_DROP:-n/a}',
'trend_ratio' => '$TREND_RATIO', 'trend_ratio' => '$TREND_RATIO',
'baseline_runs' => '$BASELINE_RUNS_USED', 'baseline_runs' => '$BASELINE_RUNS_USED',
'battery_age' => '${BATTERY_AGE:-necunoscuta}',
'battery_install' => '${BATTERY_INSTALL:-necunoscuta}',
'min_charge' => '$MIN_CHARGE', 'min_charge' => '$MIN_CHARGE',
'min_voltage' => '$MIN_VOLTAGE', 'min_voltage' => '$MIN_VOLTAGE',
'before_charge' => '$BEFORE_CHARGE', 'before_charge' => '$BEFORE_CHARGE',
@@ -560,6 +619,7 @@ log "Sănătate baterie: $BATTERY_HEALTH"
log "Scădere încărcare: $CHARGE_DROP%" log "Scădere încărcare: $CHARGE_DROP%"
log "Scădere tensiune: $VOLTAGE_DROP V" log "Scădere tensiune: $VOLTAGE_DROP V"
log "Linie de bază: ${BASELINE_DROP:-n/a} V | recent: ${RECENT_DROP:-n/a} V | raport: ${TREND_RATIO}x" log "Linie de bază: ${BASELINE_DROP:-n/a} V | recent: ${RECENT_DROP:-n/a} V | raport: ${TREND_RATIO}x"
log "Vârstă acumulatori: ${BATTERY_AGE:-necunoscută} luni"
log "Timp total: $RUNTIME secunde" log "Timp total: $RUNTIME secunde"
log "========================================" log "========================================"