Expand UPS telemetry, charts and raw diagnostics with power estimates

This commit is contained in:
Maxim
2026-10-15 22:34:55 +03:00
parent 46bd6e8be6
commit ead9aaa713
6 changed files with 280 additions and 20 deletions

49
ups.go
View File

@@ -2,6 +2,7 @@ package main
import (
"context"
"math"
"os/exec"
"strconv"
"strings"
@@ -10,6 +11,13 @@ import (
)
type UPSMetrics struct {
Raw map[string]string `json:"raw"`
NominalPower *float64 `json:"nominalPowerWatts"`
Power *float64 `json:"powerWatts"`
PowerEstimated bool `json:"powerEstimated"`
InputFrequencyRaw *float64 `json:"inputFrequencyRaw"`
FrequencyCorrected bool `json:"frequencyCorrected"`
Available bool `json:"available"`
Source string `json:"source"`
Name string `json:"name"`
@@ -92,14 +100,16 @@ func readAPCUPSD() (UPSMetrics, bool) {
}
raw := parseColonValues(string(output))
values := map[string]string{
"device.model": raw["MODEL"], "ups.status": raw["STATUS"],
"device.model": raw["MODEL"], "ups.realpower.nominal": raw["NOMPOWER"], "ups.status": raw["STATUS"],
"battery.charge": raw["BCHARGE"], "battery.runtime": minutesToSeconds(raw["TIMELEFT"]),
"ups.load": raw["LOADPCT"], "input.voltage": raw["LINEV"],
"output.voltage": raw["OUTPUTV"], "input.frequency": raw["LINEFREQ"],
"battery.voltage": raw["BATTV"], "ups.temperature": raw["ITEMP"],
"battery.date": raw["BATTDATE"], "battery.charge.low": raw["MBATTCHG"], "battery.runtime.low": minutesToSeconds(raw["MINTIMEL"]),
}
return upsFromValues("apcupsd", raw["UPSNAME"], values), true
m := upsFromValues("apcupsd", raw["UPSNAME"], values)
m.Raw = raw
return m, true
}
func parseColonValues(output string) map[string]string {
@@ -116,7 +126,7 @@ func parseColonValues(output string) map[string]string {
func upsFromValues(source, name string, values map[string]string) UPSMetrics {
status := values["ups.status"]
metrics := UPSMetrics{
Available: true, CommunicationOK: true, Source: source, Name: name, Model: values["device.model"],
Available: true, CommunicationOK: true, Source: source, Name: name, Model: firstNonEmpty(values["device.model"], values["ups.model"]), Raw: values,
Status: status, StatusLabel: upsStatusLabel(status),
ChargePercent: numberPointer(values["battery.charge"]), RuntimeSeconds: numberPointer(values["battery.runtime"]),
LoadPercent: numberPointer(values["ups.load"]), InputVoltage: numberPointer(values["input.voltage"]),
@@ -124,13 +134,44 @@ func upsFromValues(source, name string, values map[string]string) UPSMetrics {
BatteryVoltage: numberPointer(values["battery.voltage"]), Temperature: numberPointer(firstNonEmpty(values["ups.temperature"], values["battery.temperature"])),
BatteryDate: firstNonEmpty(values["battery.date"], values["battery.mfr.date"]), ReplaceBattery: strings.Contains(status, "RB"), LowBatteryCharge: numberPointer(values["battery.charge.low"]), LowBatteryRuntime: numberPointer(values["battery.runtime.low"]),
}
metrics.NominalPower = numberPointer(values["ups.realpower.nominal"])
metrics.Power = numberPointer(values["ups.realpower"])
if metrics.Power != nil && *metrics.Power < 0 {
metrics.Power = nil
}
if metrics.NominalPower != nil && *metrics.NominalPower <= 0 {
metrics.NominalPower = nil
}
if metrics.Power == nil && metrics.NominalPower != nil && metrics.LoadPercent != nil && *metrics.LoadPercent >= 0 {
watts := *metrics.NominalPower * *metrics.LoadPercent / 100
if !math.IsInf(watts, 0) {
metrics.Power = &watts
metrics.PowerEstimated = true
}
}
metrics.InputFrequencyRaw = metrics.InputFrequency
mfr := strings.ToLower(firstNonEmpty(values["device.mfr"], values["ups.mfr"]))
cyberpower := mfr == "cps" || strings.Contains(mfr, "cyberpower") || values["ups.vendorid"] == "0764"
if source == "NUT" && cyberpower && metrics.InputFrequency != nil && *metrics.InputFrequency >= 450 && *metrics.InputFrequency <= 650 {
hz := *metrics.InputFrequency / 10
metrics.InputFrequency = &hz
metrics.FrequencyCorrected = true
}
if metrics.BatteryDate != "" {
validDate := false
for _, layout := range []string{"2006/01/02", "2006-01-02", "01/02/2006", "02/01/2006"} {
if date, err := time.Parse(layout, metrics.BatteryDate); err == nil {
if date.After(time.Now()) {
continue
}
validDate = true
metrics.BatteryAgeDays = int(time.Since(date).Hours() / 24)
break
}
}
if !validDate {
metrics.BatteryDate = ""
}
}
return metrics
}
@@ -141,7 +182,7 @@ func numberPointer(value string) *float64 {
return nil
}
number, err := strconv.ParseFloat(strings.TrimSuffix(field[0], "%"), 64)
if err != nil {
if err != nil || math.IsNaN(number) || math.IsInf(number, 0) {
return nil
}
return &number