#!/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="" 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 } # Parola vine din /etc/nut/ups-shutdown.conf (nu mai e in clar in script). [[ -r /etc/nut/ups-shutdown.conf ]] && source /etc/nut/ups-shutdown.conf [[ -n "$UPS_PASS" ]] || { log "EROARE: UPS_PASS lipseste din /etc/nut/ups-shutdown.conf"; exit 1; } # 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'

[BATTERY] UPS Battery Test Report

Hostname: {{ hostname }}
Date: {{ test_date }}
UPS: {{ ups_name }}

{{ health_emoji }} Battery Health: {{ health_status }}

{{ health_description }}

Test Metrics

Charge Drop
{{ charge_drop }}%
Voltage Drop
{{ voltage_drop }}V
Min Charge
{{ min_charge }}%
Recovery Time
{{ recovery_time }}s
Trend vs Baseline
{{ trend_ratio }}x
Baseline Drop
{{ baseline_drop }}V
Battery Age
{{ battery_age }} luni

Detailed Measurements

Parameter Before Test After Test
Battery Charge {{ before_charge }}% {{ after_charge }}%
Battery Voltage {{ before_voltage }}V {{ after_voltage }}V
UPS Load {{ before_load }}% {{ after_load }}%

📋 Recommendations

{{{ recommendations }}}
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="
  • 📅 Acumulatorii au $BATTERY_AGE luni (instalați $BATTERY_INSTALL). VRLA de 7Ah intră în zona de uzură la 3-4 ani — bugetează înlocuirea.
  • " ;; 2) AGE_NOTE="
  • ⚠️ Acumulatorii au $BATTERY_AGE luni (instalați $BATTERY_INSTALL), peste 4 ani. Verdictul e coborât la FAIR pe criteriu de vârstă, indiferent de tendință.
  • " ;; esac if [ -z "$BATTERY_AGE" ]; then AGE_NOTE="
  • Vârsta acumulatorilor e necunoscută — scrie data instalării în $BATTERY_DATE_FILE (YYYY-MM-DD).
  • " 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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#\$#${AGE_NOTE}#") 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