Initial Proxmox dashboard with host and guest metrics
This commit is contained in:
158
ups.go
Normal file
158
ups.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UPSMetrics struct {
|
||||
Available bool `json:"available"`
|
||||
Source string `json:"source"`
|
||||
Name string `json:"name"`
|
||||
Model string `json:"model"`
|
||||
Status string `json:"status"`
|
||||
StatusLabel string `json:"statusLabel"`
|
||||
ChargePercent *float64 `json:"chargePercent"`
|
||||
RuntimeSeconds *float64 `json:"runtimeSeconds"`
|
||||
LoadPercent *float64 `json:"loadPercent"`
|
||||
InputVoltage *float64 `json:"inputVoltage"`
|
||||
OutputVoltage *float64 `json:"outputVoltage"`
|
||||
InputFrequency *float64 `json:"inputFrequency"`
|
||||
BatteryVoltage *float64 `json:"batteryVoltage"`
|
||||
Temperature *float64 `json:"temperatureCelsius"`
|
||||
}
|
||||
|
||||
type upsCollector struct {
|
||||
mu sync.Mutex
|
||||
cached UPSMetrics
|
||||
updatedAt time.Time
|
||||
}
|
||||
|
||||
func (c *upsCollector) collect() UPSMetrics {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if time.Since(c.updatedAt) < 5*time.Second {
|
||||
return c.cached
|
||||
}
|
||||
if metrics, ok := readNUT(); ok {
|
||||
c.cached = metrics
|
||||
} else if metrics, ok := readAPCUPSD(); ok {
|
||||
c.cached = metrics
|
||||
} else {
|
||||
c.cached = UPSMetrics{}
|
||||
}
|
||||
c.updatedAt = time.Now()
|
||||
return c.cached
|
||||
}
|
||||
|
||||
func readNUT() (UPSMetrics, bool) {
|
||||
if _, err := exec.LookPath("upsc"); err != nil {
|
||||
return UPSMetrics{}, false
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
|
||||
defer cancel()
|
||||
list, err := exec.CommandContext(ctx, "upsc", "-l").Output()
|
||||
if err != nil || strings.TrimSpace(string(list)) == "" {
|
||||
return UPSMetrics{}, false
|
||||
}
|
||||
name := strings.Fields(string(list))[0]
|
||||
output, err := exec.CommandContext(ctx, "upsc", name).Output()
|
||||
if err != nil {
|
||||
return UPSMetrics{}, false
|
||||
}
|
||||
values := parseColonValues(string(output))
|
||||
return upsFromValues("NUT", name, values), true
|
||||
}
|
||||
|
||||
func readAPCUPSD() (UPSMetrics, bool) {
|
||||
if _, err := exec.LookPath("apcaccess"); err != nil {
|
||||
return UPSMetrics{}, false
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
|
||||
defer cancel()
|
||||
output, err := exec.CommandContext(ctx, "apcaccess", "status").Output()
|
||||
if err != nil {
|
||||
return UPSMetrics{}, false
|
||||
}
|
||||
raw := parseColonValues(string(output))
|
||||
values := map[string]string{
|
||||
"device.model": raw["MODEL"], "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"],
|
||||
}
|
||||
return upsFromValues("apcupsd", raw["UPSNAME"], values), true
|
||||
}
|
||||
|
||||
func parseColonValues(output string) map[string]string {
|
||||
values := make(map[string]string)
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
key, value, ok := strings.Cut(line, ":")
|
||||
if ok {
|
||||
values[strings.TrimSpace(key)] = strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func upsFromValues(source, name string, values map[string]string) UPSMetrics {
|
||||
status := values["ups.status"]
|
||||
return UPSMetrics{
|
||||
Available: true, Source: source, Name: name, Model: values["device.model"],
|
||||
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"]),
|
||||
OutputVoltage: numberPointer(values["output.voltage"]), InputFrequency: numberPointer(values["input.frequency"]),
|
||||
BatteryVoltage: numberPointer(values["battery.voltage"]), Temperature: numberPointer(firstNonEmpty(values["ups.temperature"], values["battery.temperature"])),
|
||||
}
|
||||
}
|
||||
|
||||
func numberPointer(value string) *float64 {
|
||||
field := strings.Fields(value)
|
||||
if len(field) == 0 {
|
||||
return nil
|
||||
}
|
||||
number, err := strconv.ParseFloat(strings.TrimSuffix(field[0], "%"), 64)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &number
|
||||
}
|
||||
|
||||
func minutesToSeconds(value string) string {
|
||||
if minutes := numberPointer(value); minutes != nil {
|
||||
return strconv.FormatFloat(*minutes*60, 'f', -1, 64)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func upsStatusLabel(status string) string {
|
||||
switch {
|
||||
case strings.Contains(status, "LB"):
|
||||
return "Низкий заряд"
|
||||
case strings.Contains(status, "OB"):
|
||||
return "Работа от батареи"
|
||||
case strings.Contains(status, "OL"):
|
||||
return "Питание от сети"
|
||||
case status == "ONLINE":
|
||||
return "Питание от сети"
|
||||
case status == "ONBATT":
|
||||
return "Работа от батареи"
|
||||
default:
|
||||
return firstNonEmpty(status, "Неизвестно")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user