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
627 lines
25 KiB
Bash
627 lines
25 KiB
Bash
#!/bin/bash
|
|
#
|
|
# Script de test lunar automat baterie UPS
|
|
# Rulează pe 1 ale fiecărei luni la 00:00
|
|
# Trimite raport prin notificările Proxmox (PVE::Notify)
|
|
#
|
|
# IMPORTANT: Timing-ul de citire este CRITIC!
|
|
# - Battery.charge scade DOAR între 10-40 secunde după pornirea testului
|
|
# - UPS actualizează valorile cu delay de 5-10 secunde
|
|
#
|
|
# Creat: 2025-10-06
|
|
# Autor: Claude Code
|
|
|
|
LOGFILE="/var/log/ups-monthly-test.log"
|
|
TRENDFILE="/var/log/ups-battery-trend.csv"
|
|
BATTERY_DATE_FILE="/etc/nut/battery-install-date"
|
|
UPS_NAME="nutdev1"
|
|
UPS_USER="admin"
|
|
UPS_PASS="parola99"
|
|
TEMPLATE_DIR="/etc/pve/notification-templates/default"
|
|
START_TIME=$(date +%s)
|
|
HOSTNAME=$(hostname)
|
|
FQDN=$(hostname -f)
|
|
|
|
# Funcție logging
|
|
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
|
|
|
|
# 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ță
|
|
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
|
|
|
|
# Template: Subject
|
|
cat > "$TEMPLATE_DIR/ups-battery-test-subject.txt.hbs" << 'EOFTEMPLATE'
|
|
[{{ hostname }}] UPS Battery Test - {{ health_status }}
|
|
EOFTEMPLATE
|
|
|
|
# Template: Body Text
|
|
cat > "$TEMPLATE_DIR/ups-battery-test-body.txt.hbs" << 'EOFTEMPLATE'
|
|
========================================
|
|
UPS MONTHLY BATTERY TEST REPORT
|
|
========================================
|
|
|
|
Hostname: {{ hostname }}
|
|
Date: {{ test_date }}
|
|
UPS: {{ ups_name }}
|
|
|
|
BATTERY HEALTH: {{ health_status }}
|
|
{{ health_emoji }} {{ health_description }}
|
|
|
|
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
|
|
Battery Age: {{ battery_age }} luni (instalati {{ battery_install }})
|
|
Minimum Charge Reached: {{ min_charge }}%
|
|
Minimum Voltage: {{ min_voltage }}V
|
|
Recovery Time: {{ recovery_time }}s
|
|
|
|
BEFORE TEST:
|
|
- Battery Charge: {{ before_charge }}%
|
|
- Battery Voltage: {{ before_voltage }}V
|
|
- UPS Load: {{ before_load }}%
|
|
|
|
AFTER TEST ({{ test_duration }}s):
|
|
- Battery Charge: {{ after_charge }}%
|
|
- Battery Voltage: {{ after_voltage }}V
|
|
- UPS Load: {{ after_load }}%
|
|
|
|
RECOMMENDATIONS:
|
|
{{ recommendations }}
|
|
|
|
========================================
|
|
Script: /opt/scripts/ups-monthly-test.sh
|
|
Log: /var/log/ups-monthly-test.log
|
|
========================================
|
|
EOFTEMPLATE
|
|
|
|
# Template: Body HTML
|
|
cat > "$TEMPLATE_DIR/ups-battery-test-body.html.hbs" << 'EOFTEMPLATE'
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<style>
|
|
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; }
|
|
.container { max-width: 800px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
|
h1 { color: #2c3e50; border-bottom: 3px solid #3498db; padding-bottom: 15px; margin-top: 0; }
|
|
.status-badge { display: inline-block; padding: 10px 20px; border-radius: 5px; font-weight: bold; font-size: 18px; margin: 15px 0; }
|
|
.status-excellent { background-color: #d4edda; color: #155724; border: 2px solid #28a745; }
|
|
.status-good { background-color: #d1ecf1; color: #0c5460; border: 2px solid #17a2b8; }
|
|
.status-fair { background-color: #fff3cd; color: #856404; border: 2px solid #ffc107; }
|
|
.status-poor { background-color: #f8d7da; color: #721c24; border: 2px solid #dc3545; }
|
|
.metrics { display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin: 25px 0; }
|
|
.metric { background: #ecf0f1; padding: 20px; border-radius: 5px; border-left: 4px solid #3498db; }
|
|
.metric-label { font-size: 13px; color: #7f8c8d; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
.metric-value { font-size: 28px; font-weight: bold; color: #2c3e50; margin-top: 8px; }
|
|
.section { margin: 25px 0; padding: 20px; background: #f8f9fa; border-radius: 5px; }
|
|
.section h2 { color: #34495e; margin-top: 0; font-size: 20px; }
|
|
.recommendations { background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0; }
|
|
.recommendations ul { margin: 10px 0; padding-left: 20px; }
|
|
.footer { margin-top: 30px; padding-top: 20px; border-top: 2px solid #ecf0f1; font-size: 12px; color: #7f8c8d; text-align: center; }
|
|
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
|
|
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
|
|
th { background-color: #3498db; color: white; font-weight: 600; }
|
|
tr:hover { background-color: #f5f5f5; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>[BATTERY] UPS Battery Test Report</h1>
|
|
|
|
<p><strong>Hostname:</strong> {{ hostname }}<br>
|
|
<strong>Date:</strong> {{ test_date }}<br>
|
|
<strong>UPS:</strong> {{ ups_name }}</p>
|
|
|
|
<div class="status-badge status-{{ health_class }}">
|
|
{{ health_emoji }} Battery Health: {{ health_status }}
|
|
</div>
|
|
|
|
<p style="font-size: 16px; margin-top: 15px;">{{ health_description }}</p>
|
|
|
|
<h2 style="margin-top: 30px;">Test Metrics</h2>
|
|
<div class="metrics">
|
|
<div class="metric">
|
|
<div class="metric-label">Charge Drop</div>
|
|
<div class="metric-value">{{ charge_drop }}%</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-label">Voltage Drop</div>
|
|
<div class="metric-value">{{ voltage_drop }}V</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-label">Min Charge</div>
|
|
<div class="metric-value">{{ min_charge }}%</div>
|
|
</div>
|
|
<div class="metric">
|
|
<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 class="metric">
|
|
<div class="metric-label">Battery Age</div>
|
|
<div class="metric-value">{{ battery_age }} luni</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>Detailed Measurements</h2>
|
|
<table>
|
|
<tr>
|
|
<th>Parameter</th>
|
|
<th>Before Test</th>
|
|
<th>After Test</th>
|
|
</tr>
|
|
<tr>
|
|
<td>Battery Charge</td>
|
|
<td>{{ before_charge }}%</td>
|
|
<td>{{ after_charge }}%</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Battery Voltage</td>
|
|
<td>{{ before_voltage }}V</td>
|
|
<td>{{ after_voltage }}V</td>
|
|
</tr>
|
|
<tr>
|
|
<td>UPS Load</td>
|
|
<td>{{ before_load }}%</td>
|
|
<td>{{ after_load }}%</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="recommendations">
|
|
<h2 style="margin-top: 0;">📋 Recommendations</h2>
|
|
{{{ recommendations }}}
|
|
</div>
|
|
|
|
<div class="footer">
|
|
<p><strong>Script:</strong> /opt/scripts/ups-monthly-test.sh<br>
|
|
<strong>Log File:</strong> /var/log/ups-monthly-test.log</p>
|
|
<p style="margin-top: 10px;">Proxmox VE - UPS Monitoring System</p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
EOFTEMPLATE
|
|
|
|
log "Templates created in $TEMPLATE_DIR/"
|
|
}
|
|
|
|
# Verifică și creează template-urile dacă nu există
|
|
# 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 'battery_age' "$TEMPLATE_DIR/ups-battery-test-body.txt.hbs" 2>/dev/null; then
|
|
log "Creating/updating notification templates..."
|
|
create_templates
|
|
fi
|
|
|
|
log "========================================"
|
|
log "UPS MONTHLY BATTERY TEST - START"
|
|
log "========================================"
|
|
|
|
# 1. Verificare status UPS înainte de test
|
|
log "Step 1: Verificare status UPS înainte de test..."
|
|
BEFORE_STATUS=$(upsc $UPS_NAME ups.status 2>/dev/null)
|
|
BEFORE_CHARGE=$(upsc $UPS_NAME battery.charge 2>/dev/null)
|
|
BEFORE_VOLTAGE=$(upsc $UPS_NAME battery.voltage 2>/dev/null)
|
|
BEFORE_LOAD=$(upsc $UPS_NAME ups.load 2>/dev/null)
|
|
|
|
log " Status: $BEFORE_STATUS"
|
|
log " Battery Charge: $BEFORE_CHARGE%"
|
|
log " Battery Voltage: $BEFORE_VOLTAGE V"
|
|
log " Load: $BEFORE_LOAD%"
|
|
|
|
# Verifică dacă UPS este online
|
|
if [[ $BEFORE_STATUS != *"OL"* ]]; then
|
|
log "ERROR: UPS nu este online! Status: $BEFORE_STATUS"
|
|
log "Test ANULAT"
|
|
exit 1
|
|
fi
|
|
|
|
# Verifică încărcare baterie
|
|
if [ "$BEFORE_CHARGE" -lt 95 ]; then
|
|
log "WARNING: Baterie nu este complet încărcată ($BEFORE_CHARGE%)"
|
|
fi
|
|
|
|
# 2. Pornire test baterie
|
|
log ""
|
|
log "Step 2: Pornire test baterie..."
|
|
TEST_START_TIME=$(date +%s)
|
|
|
|
upscmd -u $UPS_USER -p $UPS_PASS $UPS_NAME test.battery.start.quick 2>&1 | tee -a $LOGFILE
|
|
|
|
if [ ${PIPESTATUS[0]} -eq 0 ]; then
|
|
log "Test baterie pornit cu succes!"
|
|
else
|
|
log "ERROR: Nu am putut porni testul de baterie!"
|
|
exit 1
|
|
fi
|
|
|
|
# 3. TIMING CRITIC: Așteptare 10-15 secunde pentru ca charge să scadă
|
|
log ""
|
|
log "Step 3: Monitorizare test baterie (timing critic pentru charge drop)..."
|
|
|
|
MIN_CHARGE=$BEFORE_CHARGE
|
|
MIN_VOLTAGE=$BEFORE_VOLTAGE
|
|
CHARGE_AT_15S=$BEFORE_CHARGE
|
|
VOLTAGE_AT_15S=$BEFORE_VOLTAGE
|
|
|
|
# Primele 5 secunde - inițializare test
|
|
sleep 5
|
|
|
|
# 10-40 secunde - fereastra critică când charge scade
|
|
for i in {1..7}; do
|
|
CURRENT_CHARGE=$(upsc $UPS_NAME battery.charge 2>/dev/null)
|
|
CURRENT_VOLTAGE=$(upsc $UPS_NAME battery.voltage 2>/dev/null)
|
|
|
|
# Capturează minimul
|
|
if [ ! -z "$CURRENT_CHARGE" ] && [ "$CURRENT_CHARGE" -lt "$MIN_CHARGE" ]; then
|
|
MIN_CHARGE=$CURRENT_CHARGE
|
|
fi
|
|
|
|
if [ ! -z "$CURRENT_VOLTAGE" ]; then
|
|
MIN_VOLTAGE=$(echo "$CURRENT_VOLTAGE $MIN_VOLTAGE" | awk '{if ($1 < $2) print $1; else print $2}')
|
|
fi
|
|
|
|
# Citire la 15 secunde (punct optim)
|
|
if [ $i -eq 2 ]; then
|
|
CHARGE_AT_15S=$CURRENT_CHARGE
|
|
VOLTAGE_AT_15S=$CURRENT_VOLTAGE
|
|
log " [15s CRITICAL] Charge: $CURRENT_CHARGE% | Voltage: $CURRENT_VOLTAGE V"
|
|
else
|
|
log " [$((5 + i*5))s] Charge: $CURRENT_CHARGE% | Voltage: $CURRENT_VOLTAGE V"
|
|
fi
|
|
|
|
sleep 5
|
|
done
|
|
|
|
TEST_END_TIME=$(date +%s)
|
|
TEST_DURATION=$((TEST_END_TIME - TEST_START_TIME))
|
|
|
|
log " Minimum Charge: $MIN_CHARGE%"
|
|
log " Minimum Voltage: $MIN_VOLTAGE V"
|
|
|
|
# 4. Așteptare recuperare și citire finală
|
|
log ""
|
|
log "Step 4: Așteptare recuperare baterie (15 secunde)..."
|
|
sleep 15
|
|
|
|
AFTER_STATUS=$(upsc $UPS_NAME ups.status 2>/dev/null)
|
|
AFTER_CHARGE=$(upsc $UPS_NAME battery.charge 2>/dev/null)
|
|
AFTER_VOLTAGE=$(upsc $UPS_NAME battery.voltage 2>/dev/null)
|
|
AFTER_LOAD=$(upsc $UPS_NAME ups.load 2>/dev/null)
|
|
|
|
log " Status: $AFTER_STATUS"
|
|
log " Battery Charge: $AFTER_CHARGE%"
|
|
log " Battery Voltage: $AFTER_VOLTAGE V"
|
|
log " Load: $AFTER_LOAD%"
|
|
|
|
# 5. Calcul metrici
|
|
CHARGE_DROP=$((BEFORE_CHARGE - MIN_CHARGE))
|
|
VOLTAGE_DROP=$(echo "$BEFORE_VOLTAGE - $MIN_VOLTAGE" | bc 2>/dev/null || echo "0")
|
|
|
|
# Rotunjire voltage drop la 2 zecimale
|
|
VOLTAGE_DROP=$(printf "%.2f" $VOLTAGE_DROP 2>/dev/null || echo $VOLTAGE_DROP)
|
|
|
|
log ""
|
|
log "Step 5: Analiza rezultate test..."
|
|
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 — 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"
|
|
|
|
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)
|
|
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"
|
|
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"
|
|
HEALTH_CLASS="fair"
|
|
HEALTH_EMOJI="[INFO]"
|
|
HEALTH_DESCRIPTION=""
|
|
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 —
|
|
# 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" ] && [ "$AGE_CLASS" -lt 2 ]; then
|
|
BATTERY_HEALTH="BASELINE"
|
|
HEALTH_CLASS="good"
|
|
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="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" -v a="$AGE_CLASS" 'BEGIN {
|
|
c = (r < 1.25) ? 1 : (r < 1.50) ? 2 : (r < 2.00) ? 3 : 4
|
|
if (f == "warn" && c < 3) c = 3
|
|
if (a == 2 && 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
|
|
|
|
# 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)
|
|
log ""
|
|
log "Step 6: Monitorizare recuperare baterie..."
|
|
|
|
RECOVERY_START=$(date +%s)
|
|
sleep 30
|
|
RECOVERY_CHARGE=$(upsc $UPS_NAME battery.charge 2>/dev/null)
|
|
RECOVERY_TIME=$(($(date +%s) - RECOVERY_START))
|
|
|
|
log " Charge după $RECOVERY_TIME secunde: $RECOVERY_CHARGE%"
|
|
|
|
# 8. Calculează timpul total
|
|
END_TIME=$(date +%s)
|
|
RUNTIME=$((END_TIME - START_TIME))
|
|
|
|
# 9. Determină severity pentru notificare
|
|
if [ "$BATTERY_HEALTH" = "EXCELLENT" ] || [ "$BATTERY_HEALTH" = "GOOD" ] || [ "$BATTERY_HEALTH" = "BASELINE" ]; then
|
|
SEVERITY="info"
|
|
elif [ "$BATTERY_HEALTH" = "FAIR" ] || [ "$BATTERY_HEALTH" = "UNKNOWN" ]; then
|
|
SEVERITY="warning"
|
|
else
|
|
SEVERITY="error"
|
|
fi
|
|
|
|
# 10. Trimite notificarea prin PVE::Notify
|
|
log ""
|
|
log "Step 7: Trimitere notificare prin PVE::Notify..."
|
|
|
|
# Escape pentru Perl heredoc
|
|
RECOMMENDATIONS_ESCAPED=$(echo "$RECOMMENDATIONS" | sed "s/'/\\'/g")
|
|
|
|
perl -I/usr/share/perl5 << EOFPERL
|
|
use strict;
|
|
use warnings;
|
|
use PVE::Notify;
|
|
|
|
my \$template_data = {
|
|
'hostname' => '$FQDN',
|
|
'test_date' => '$(date '+%Y-%m-%d %H:%M:%S')',
|
|
'ups_name' => '$UPS_NAME',
|
|
'health_status' => '$BATTERY_HEALTH',
|
|
'health_class' => '$HEALTH_CLASS',
|
|
'health_emoji' => '$HEALTH_EMOJI',
|
|
'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',
|
|
'battery_age' => '${BATTERY_AGE:-necunoscuta}',
|
|
'battery_install' => '${BATTERY_INSTALL:-necunoscuta}',
|
|
'min_charge' => '$MIN_CHARGE',
|
|
'min_voltage' => '$MIN_VOLTAGE',
|
|
'before_charge' => '$BEFORE_CHARGE',
|
|
'before_voltage' => '$BEFORE_VOLTAGE',
|
|
'before_load' => '$BEFORE_LOAD',
|
|
'after_charge' => '$AFTER_CHARGE',
|
|
'after_voltage' => '$AFTER_VOLTAGE',
|
|
'after_load' => '$AFTER_LOAD',
|
|
'test_duration' => '$TEST_DURATION',
|
|
'recovery_time' => '$RECOVERY_TIME',
|
|
'recommendations' => '$RECOMMENDATIONS_ESCAPED'
|
|
};
|
|
|
|
my \$fields = {
|
|
'hostname' => '$HOSTNAME',
|
|
'type' => 'ups-battery-test',
|
|
'health' => '$BATTERY_HEALTH'
|
|
};
|
|
|
|
eval {
|
|
PVE::Notify::notify('$SEVERITY', 'ups-battery-test', \$template_data, \$fields);
|
|
print "Notification sent successfully\\n";
|
|
};
|
|
if (\$@) {
|
|
print STDERR "Failed to send notification: \$@\\n";
|
|
exit 1;
|
|
}
|
|
EOFPERL
|
|
|
|
PERL_EXIT_CODE=$?
|
|
|
|
if [ $PERL_EXIT_CODE -eq 0 ]; then
|
|
log "Notificare trimisă cu succes prin PVE::Notify"
|
|
else
|
|
log "ERROR: Notificarea a eșuat (exit code: $PERL_EXIT_CODE)"
|
|
fi
|
|
|
|
# 11. Finalizare
|
|
log ""
|
|
log "========================================"
|
|
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 "Vârstă acumulatori: ${BATTERY_AGE:-necunoscută} luni"
|
|
log "Timp total: $RUNTIME secunde"
|
|
log "========================================"
|
|
|
|
exit 0
|