fix(ups): testul lunar de baterie nu putea da alarma niciodata

Verdictul se calcula din CHARGE_DROP, dar la acest UPS battery.charge nu e o
masuratoare independenta: driverul nutdrv_qx / Voltronic-QS-Hex o deduce liniar
din tensiune intre battery.voltage.low (20.8V) si .high (26.0V). Verificat pe
toate probele din 11 luni: 25.76V -> 95.4% -> raportat 95%.

Traduse in tensiune, pragurile vechi cereau o cadere de 3.70V pentru FAIR (primul
prag care trimite severity warning) si 4.74V pentru POOR, intr-un test de 40 de
secunde. Maximul observat in 11 luni a fost 2.59V. De asta raportul a iesit
EXCELLENT 11 luni la rand si ar fi facut-o si cu bateria pe moarte.

Evaluarea se face acum pe tendinta: mediana ultimelor 3 rulari fata de mediana
primelor 6, plus o plasa de siguranta pe tensiune absoluta (<25.0V forteaza FAIR,
<24.0V forteaza POOR). Fereastra de 3 impiedica o luna atipica sa declanseze
singura alarma - verificat pe feb 2026 (2.59V), care nu bascuelaza verdictul.

Istoricul se tine in /var/log/ups-battery-trend.csv, populat cu cele 14 rulari
extrase din jurnal (2025-10-06 -> 2026-08-01), ca linia de baza sa fie valida
imediat si nu peste 9 luni.

Starea la zi: baza 1.82V, recent 2.12V, raport 1.16x -> EXCELLENT. Cresterea de
~16% in 10 luni e reala dar sub banda de avertizare; FAIR s-ar da la 2.73V.

Instalat pe pvemini in /opt/scripts/, verificat cap-coada cu testul de baterie
anulat, ca sa nu descarce bateria: tendinta calculata corect, template-urile
regenerate cu campurile noi, notificarea PVE::Notify trimisa cu succes.

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 14:46:35 +03:00
parent bf73ccf5b1
commit 0b8e8f6892
4 changed files with 368 additions and 50 deletions

View File

@@ -0,0 +1,102 @@
#!/bin/bash
#
# Reconstruiește /var/log/ups-battery-trend.csv din jurnalul testelor lunare.
#
# De ce există: ups-monthly-test.sh evaluează sănătatea bateriei comparând
# căderea de tensiune cu linia de bază a acestei baterii. Fără istoric, linia de
# bază s-ar forma abia după 9 rulări, adică 9 luni. Jurnalul conține însă deja
# măsurătorile, din 2025-10-06 încoace — se extrag de acolo.
#
# Rulare unică. Refuză să suprascrie un CSV existent fără --force, ca să nu
# reseteze din greșeală linia de bază acumulată.
#
# Creat: 2026-08-27
set -euo pipefail
LOGFILE="${LOGFILE:-/var/log/ups-monthly-test.log}"
TRENDFILE="${TRENDFILE:-/var/log/ups-battery-trend.csv}"
FORCE=0
DRY_RUN=0
for arg in "$@"; do
case "$arg" in
--force) FORCE=1 ;;
--dry-run) DRY_RUN=1 ;;
*) echo "Argument necunoscut: $arg" >&2; exit 2 ;;
esac
done
if [ ! -f "$LOGFILE" ]; then
echo "EROARE: nu găsesc jurnalul $LOGFILE" >&2
exit 1
fi
if [ -f "$TRENDFILE" ] && [ "$FORCE" -eq 0 ] && [ "$DRY_RUN" -eq 0 ]; then
echo "EROARE: $TRENDFILE există deja. Folosește --force ca să-l reconstruiești." >&2
exit 1
fi
# Parserul lucrează doar cu tipare ASCII — etichetele cu diacritice din jurnal
# ("Scădere tensiune") nu se pot potrivi portabil în awk pe toate locale-urile,
# așa că valorile se recalculează din before/min, care apar fără diacritice.
parse_log() {
awk '
function num(s) { gsub(/[^0-9.]/, "", s); return s }
/UPS MONTHLY BATTERY TEST - START/ {
active = 1; phase = 0
bch = ""; bv = ""; bl = ""; mc = ""; mv = ""; al = ""; dur = ""; d = ""
next
}
!active { next }
/Step 1:/ { phase = 1; next }
/Step 3:/ { phase = 3; d = substr($1, 2); next }
/Step 4:/ { phase = 4; next }
phase == 1 && /Battery Charge:/ { bch = num($NF); next }
phase == 1 && /Battery Voltage:/ { bv = num($(NF-1)); next }
phase == 1 && /Load:/ { bl = num($NF); next }
/Minimum Charge:/ { mc = num($NF); next }
/Minimum Voltage:/ { mv = num($(NF-1)); next }
phase == 4 && /Load:/ { al = num($NF); next }
/ test: [0-9]+ secunde/ { dur = num($(NF-1)); next }
/UPS MONTHLY BATTERY TEST - COMPLETE/ {
active = 0
# Blocurile din versiunile vechi de script nu au minimele — se sar.
if (d != "" && mv != "" && bv != "") {
vd = bv - mv
cd = (bch != "" && mc != "") ? bch - mc : ""
printf "%s,%.2f,%s,%s,%s,%s,%s\n", d, vd, mv, cd, bl, al, dur
}
next
}
' "$LOGFILE"
}
ROWS=$(parse_log)
COUNT=$(printf '%s\n' "$ROWS" | grep -c ',' || true)
if [ "$COUNT" -eq 0 ]; then
echo "EROARE: nu s-a extras nicio rulare din $LOGFILE" >&2
exit 1
fi
if [ "$DRY_RUN" -eq 1 ]; then
echo "date,voltage_drop,min_voltage,charge_drop,before_load,after_load,duration"
printf '%s\n' "$ROWS"
echo "--- $COUNT rulari extrase (dry-run, nimic scris) ---" >&2
exit 0
fi
{
echo "date,voltage_drop,min_voltage,charge_drop,before_load,after_load,duration"
printf '%s\n' "$ROWS"
} > "$TRENDFILE"
echo "Scris $COUNT rulari in $TRENDFILE"

View File

@@ -12,6 +12,7 @@
# Autor: Claude Code
LOGFILE="/var/log/ups-monthly-test.log"
TRENDFILE="/var/log/ups-battery-trend.csv"
UPS_NAME="nutdev1"
UPS_USER="admin"
UPS_PASS="parola99"
@@ -25,6 +26,41 @@ log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a $LOGFILE
}
# Câte rulări formează linia de bază și câte intră în fereastra recentă.
# Baza = primele rulări înregistrate, adică bateria în starea ei cea mai bună
# pe care o avem măsurată. Fereastra recentă e mediană pe 3, ca o lună
# atipică (frig, sarcină mare) să nu declanșeze singură o alarmă.
BASELINE_RUNS=6
TREND_WINDOW=3
# Adaugă rularea curentă în fișierul de tendință
append_trend() {
if [ ! -f "$TRENDFILE" ]; then
echo "date,voltage_drop,min_voltage,charge_drop,before_load,after_load,duration" > "$TRENDFILE"
fi
echo "$(date '+%Y-%m-%d'),$1,$2,$3,$4,$5,$6" >> "$TRENDFILE"
}
# Numărul de rulări înregistrate (fără antet)
trend_rows() {
tail -n +2 "$TRENDFILE" 2>/dev/null | grep -c ',' || echo 0
}
# Mediana coloanei voltage_drop. $1 = head|tail (cele mai vechi / cele mai noi),
# $2 = câte rânduri. head/tail se aplică pe ordinea cronologică, sortarea abia
# după — altfel s-ar lua cele mai mici valori, nu cele mai vechi.
trend_median() {
local mode="$1" n="$2"
tail -n +2 "$TRENDFILE" 2>/dev/null | cut -d, -f2 | grep -E '^[0-9]+\.?[0-9]*$' \
| { if [ "$mode" = "head" ]; then head -n "$n"; else tail -n "$n"; fi; } \
| sort -g \
| awk '{v[NR]=$1} END {
if (NR == 0) { print "" ; exit }
if (NR % 2) printf "%.2f", v[(NR+1)/2]
else printf "%.2f", (v[NR/2] + v[NR/2+1]) / 2
}'
}
# Funcție pentru crearea template-urilor de notificare
create_templates() {
mkdir -p $TEMPLATE_DIR
@@ -51,6 +87,9 @@ TEST RESULTS:
-------------
Battery Charge Drop: {{ charge_drop }}%
Battery Voltage Drop: {{ voltage_drop }}V
Baseline Voltage Drop: {{ baseline_drop }}V ({{ baseline_runs }} rulari de referinta)
Recent Median (3 run): {{ recent_drop }}V
Trend vs Baseline: {{ trend_ratio }}x
Minimum Charge Reached: {{ min_charge }}%
Minimum Voltage: {{ min_voltage }}V
Recovery Time: {{ recovery_time }}s
@@ -136,6 +175,14 @@ EOFTEMPLATE
<div class="metric-label">Recovery Time</div>
<div class="metric-value">{{ recovery_time }}s</div>
</div>
<div class="metric">
<div class="metric-label">Trend vs Baseline</div>
<div class="metric-value">{{ trend_ratio }}x</div>
</div>
<div class="metric">
<div class="metric-label">Baseline Drop</div>
<div class="metric-value">{{ baseline_drop }}V</div>
</div>
</div>
<div class="section">
@@ -183,8 +230,11 @@ EOFTEMPLATE
}
# Verifică și creează template-urile dacă nu există
if [ ! -f "$TEMPLATE_DIR/ups-battery-test-subject.txt.hbs" ]; then
log "Creating notification templates..."
# 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ă.
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
log "Creating/updating notification templates..."
create_templates
fi
@@ -302,41 +352,117 @@ log " Durată test: $TEST_DURATION secunde"
log " Scădere încărcare: $CHARGE_DROP% (de la $BEFORE_CHARGE% la $MIN_CHARGE%)"
log " Scădere tensiune: $VOLTAGE_DROP V (de la $BEFORE_VOLTAGE V la $MIN_VOLTAGE V)"
# 6. Evaluare sănătate baterie
# 6. Evaluare sănătate baterie — pe tendință, nu pe prag absolut
#
# De ce nu pe prag absolut: driverul nu măsoară încărcarea, o deduce liniar din
# tensiune între battery.voltage.low (20.8V) și .high (26.0V). Un CHARGE_DROP de
# 15% înseamnă deci o cădere sub ~25.2V, iar 55% înseamnă sub ~23.1V — într-un
# test de 40 de secunde. O baterie ajunsă acolo e deja moartă. Pragurile vechi
# nu se puteau atinge, așa că verdictul a ieșit EXCELLENT 11 luni la rând.
#
# Ce se măsoară în schimb: căderea de tensiune sub sarcină, comparată cu linia de
# bază a acestei baterii. Rezistența internă crește pe măsură ce bateria
# îmbătrânește, deci raportul crește lent și dă avertisment din timp.
append_trend "$VOLTAGE_DROP" "$MIN_VOLTAGE" "$CHARGE_DROP" "$BEFORE_LOAD" "$AFTER_LOAD" "$TEST_DURATION"
TREND_ROWS=$(trend_rows)
BASELINE_DROP=$(trend_median head $BASELINE_RUNS)
RECENT_DROP=$(trend_median tail $TREND_WINDOW)
BASELINE_RUNS_USED=$BASELINE_RUNS
if [ "$TREND_ROWS" -lt "$BASELINE_RUNS" ]; then
BASELINE_RUNS_USED=$TREND_ROWS
fi
TREND_RATIO="n/a"
if [ -n "$BASELINE_DROP" ] && [ -n "$RECENT_DROP" ]; then
TREND_RATIO=$(awk -v r="$RECENT_DROP" -v b="$BASELINE_DROP" \
'BEGIN { if (b > 0) printf "%.2f", r / b; else print "n/a" }')
fi
log ""
log "Step 5b: Tendință față de linia de bază..."
log " Rulări înregistrate: $TREND_ROWS (fișier: $TRENDFILE)"
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 " Raport: ${TREND_RATIO}x"
BATTERY_HEALTH="UNKNOWN"
HEALTH_CLASS="fair"
HEALTH_EMOJI="[INFO]"
HEALTH_DESCRIPTION=""
RECOMMENDATIONS=""
if [ "$CHARGE_DROP" -lt 15 ]; then
BATTERY_HEALTH="EXCELLENT"
HEALTH_CLASS="excellent"
HEALTH_EMOJI="[OK]"
HEALTH_DESCRIPTION="Battery is in excellent condition with minimal discharge during test."
RECOMMENDATIONS="<ul><li>✅ Battery is healthy and functioning normally</li><li>Continue monthly testing</li><li>No action required</li></ul>"
log " Sănătate baterie: EXCELENTĂ (scădere < 15%)"
elif [ "$CHARGE_DROP" -lt 35 ]; then
BATTERY_HEALTH="GOOD"
# 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.
FLOOR=$(awk -v v="$MIN_VOLTAGE" 'BEGIN {
if (v == "" ) { print "none" }
else if (v < 24.0) { print "critical" }
else if (v < 25.0) { print "warn" }
else { print "none" }
}')
if [ "$TREND_ROWS" -lt $((BASELINE_RUNS + TREND_WINDOW)) ] && [ "$FLOOR" = "none" ]; then
BATTERY_HEALTH="BASELINE"
HEALTH_CLASS="good"
HEALTH_EMOJI="[OK]"
HEALTH_DESCRIPTION="Battery shows normal wear but performs adequately."
RECOMMENDATIONS="<ul><li>Battery is functioning well</li><li>Monitor monthly for degradation trends</li><li>No immediate action needed</li></ul>"
log " Sănătate baterie: BUNĂ (scădere 15-35%)"
elif [ "$CHARGE_DROP" -lt 55 ]; then
BATTERY_HEALTH="FAIR"
HEALTH_CLASS="fair"
HEALTH_EMOJI="[WARNING]"
HEALTH_DESCRIPTION="Battery shows significant wear and should be monitored closely."
RECOMMENDATIONS="<ul><li>⚠️ Battery is aging</li><li>Plan replacement in 3-6 months</li><li>Increase monitoring frequency</li><li>Order replacement battery soon</li></ul>"
log " Sănătate baterie: ACCEPTABILĂ (scădere 35-55%)"
else
HEALTH_EMOJI="[INFO]"
HEALTH_DESCRIPTION="Se colectează linia de bază. Verdictul pe tendință apare după $((BASELINE_RUNS + TREND_WINDOW)) rulări (acum: $TREND_ROWS)."
RECOMMENDATIONS="<ul><li>Istoric insuficient pentru o comparație pe tendință</li><li>Rulări înregistrate: $TREND_ROWS din $((BASELINE_RUNS + TREND_WINDOW)) necesare</li><li>Nicio acțiune necesară</li></ul>"
log " Sănătate baterie: BASELINE (istoric insuficient)"
elif [ "$FLOOR" = "critical" ]; then
BATTERY_HEALTH="POOR"
HEALTH_CLASS="poor"
HEALTH_EMOJI="[CRITICAL]"
HEALTH_DESCRIPTION="Battery is critically weak and requires immediate replacement!"
RECOMMENDATIONS="<ul><li>🔴 <strong>URGENT:</strong> Battery needs immediate replacement!</li><li>Order new battery NOW</li><li>UPS may not provide adequate protection</li><li>Risk of unexpected shutdown</li></ul>"
log " Sănătate baterie: SLABĂ (scădere > 55%) - NECESITĂ ÎNLOCUIRE!"
HEALTH_DESCRIPTION="Tensiunea a coborât sub 24.0V într-un test de $TEST_DURATION secunde. Bateria nu mai susține sarcina."
RECOMMENDATIONS="<ul><li>🔴 <strong>URGENT:</strong> înlocuiește bateria acum</li><li>Minim atins: ${MIN_VOLTAGE}V (cutoff UPS: 20.8V)</li><li>Autonomia reală e de ordinul secundelor, nu al minutelor</li><li>Shutdown-ul orchestrat s-ar putea să nu apuce să termine</li></ul>"
log " Sănătate baterie: SLABĂ (minim ${MIN_VOLTAGE}V < 24.0V) - NECESITĂ ÎNLOCUIRE!"
elif [ "$TREND_RATIO" = "n/a" ]; then
BATTERY_HEALTH="UNKNOWN"
HEALTH_CLASS="fair"
HEALTH_EMOJI="[WARNING]"
HEALTH_DESCRIPTION="Nu s-a putut calcula tendința (fișier de tendință gol sau corupt)."
RECOMMENDATIONS="<ul><li>Verifică $TRENDFILE</li><li>Verifică dacă upsc întoarce battery.voltage</li></ul>"
log " Sănătate baterie: NECUNOSCUTĂ (tendință incalculabilă)"
else
RATIO_CLASS=$(awk -v r="$TREND_RATIO" -v f="$FLOOR" 'BEGIN {
c = (r < 1.25) ? 1 : (r < 1.50) ? 2 : (r < 2.00) ? 3 : 4
if (f == "warn" && c < 3) c = 3
print c
}')
case "$RATIO_CLASS" in
1)
BATTERY_HEALTH="EXCELLENT"
HEALTH_CLASS="excellent"
HEALTH_EMOJI="[OK]"
HEALTH_DESCRIPTION="Căderea de tensiune sub sarcină e la nivelul liniei de bază (${TREND_RATIO}x). Bateria nu dă semne de îmbătrânire."
RECOMMENDATIONS="<ul><li>✅ Bateria e sănătoasă</li><li>Recent ${RECENT_DROP}V față de baza ${BASELINE_DROP}V</li><li>Nicio acțiune necesară</li></ul>"
log " Sănătate baterie: EXCELENTĂ (${TREND_RATIO}x față de bază)"
;;
2)
BATTERY_HEALTH="GOOD"
HEALTH_CLASS="good"
HEALTH_EMOJI="[OK]"
HEALTH_DESCRIPTION="Căderea de tensiune a crescut cu 25-50% față de linia de bază (${TREND_RATIO}x). Uzură normală."
RECOMMENDATIONS="<ul><li>Îmbătrânire vizibilă, dar în limite normale</li><li>Recent ${RECENT_DROP}V față de baza ${BASELINE_DROP}V</li><li>Urmărește lunar; la 1.5x se planifică înlocuirea</li></ul>"
log " Sănătate baterie: BUNĂ (${TREND_RATIO}x față de bază)"
;;
3)
BATTERY_HEALTH="FAIR"
HEALTH_CLASS="fair"
HEALTH_EMOJI="[WARNING]"
HEALTH_DESCRIPTION="Căderea de tensiune s-a mărit de ${TREND_RATIO}x față de linia de bază. Rezistența internă a crescut semnificativ."
RECOMMENDATIONS="<ul><li>⚠️ Planifică înlocuirea în 3-6 luni</li><li>Recent ${RECENT_DROP}V față de baza ${BASELINE_DROP}V</li><li>Minim atins în test: ${MIN_VOLTAGE}V</li><li>Comandă bateriile de schimb (2x 12V 7Ah)</li></ul>"
log " Sănătate baterie: ACCEPTABILĂ (${TREND_RATIO}x față de bază)"
;;
*)
BATTERY_HEALTH="POOR"
HEALTH_CLASS="poor"
HEALTH_EMOJI="[CRITICAL]"
HEALTH_DESCRIPTION="Căderea de tensiune s-a dublat față de linia de bază (${TREND_RATIO}x). Bateria și-a pierdut capacitatea."
RECOMMENDATIONS="<ul><li>🔴 <strong>URGENT:</strong> înlocuiește bateria</li><li>Recent ${RECENT_DROP}V față de baza ${BASELINE_DROP}V</li><li>Autonomia nu mai acoperă procedura de shutdown orchestrat</li><li>Vezi proxmox/cluster/ups/docs/UPS-BATTERY-REPLACEMENT.md</li></ul>"
log " Sănătate baterie: SLABĂ (${TREND_RATIO}x față de bază) - NECESITĂ ÎNLOCUIRE!"
;;
esac
fi
# 7. Monitorizare recuperare (30 secunde)
@@ -355,9 +481,9 @@ END_TIME=$(date +%s)
RUNTIME=$((END_TIME - START_TIME))
# 9. Determină severity pentru notificare
if [ "$BATTERY_HEALTH" = "EXCELLENT" ] || [ "$BATTERY_HEALTH" = "GOOD" ]; then
if [ "$BATTERY_HEALTH" = "EXCELLENT" ] || [ "$BATTERY_HEALTH" = "GOOD" ] || [ "$BATTERY_HEALTH" = "BASELINE" ]; then
SEVERITY="info"
elif [ "$BATTERY_HEALTH" = "FAIR" ]; then
elif [ "$BATTERY_HEALTH" = "FAIR" ] || [ "$BATTERY_HEALTH" = "UNKNOWN" ]; then
SEVERITY="warning"
else
SEVERITY="error"
@@ -385,6 +511,10 @@ my \$template_data = {
'health_description' => '$HEALTH_DESCRIPTION',
'charge_drop' => '$CHARGE_DROP',
'voltage_drop' => '$VOLTAGE_DROP',
'baseline_drop' => '${BASELINE_DROP:-n/a}',
'recent_drop' => '${RECENT_DROP:-n/a}',
'trend_ratio' => '$TREND_RATIO',
'baseline_runs' => '$BASELINE_RUNS_USED',
'min_charge' => '$MIN_CHARGE',
'min_voltage' => '$MIN_VOLTAGE',
'before_charge' => '$BEFORE_CHARGE',
@@ -429,6 +559,7 @@ log "UPS MONTHLY BATTERY TEST - COMPLETE"
log "Sănătate baterie: $BATTERY_HEALTH"
log "Scădere încărcare: $CHARGE_DROP%"
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 "Timp total: $RUNTIME secunde"
log "========================================"