package ups import ( "bufio" "context" "fmt" "os/exec" "strconv" "strings" ) type Sample struct { OnBattery bool LowBattery bool RuntimeSeconds int RawStatus string } type Provider interface { Read(context.Context) (Sample, error) } type NUTProvider struct { Target string } func NewNUTProvider(target string) *NUTProvider { return &NUTProvider{Target: target} } func (p *NUTProvider) Read(ctx context.Context) (Sample, error) { if p.Target == "" { return Sample{}, fmt.Errorf("NUT target must not be empty") } cmd := exec.CommandContext(ctx, "upsc", p.Target) out, err := cmd.Output() if err != nil { return Sample{}, fmt.Errorf("upsc %s: %w", p.Target, err) } return parseNUT(string(out)) } func parseNUT(raw string) (Sample, error) { kv := map[string]string{} s := bufio.NewScanner(strings.NewReader(raw)) for s.Scan() { line := strings.TrimSpace(s.Text()) if line == "" { continue } parts := strings.SplitN(line, ":", 2) if len(parts) != 2 { continue } k := strings.TrimSpace(parts[0]) v := strings.TrimSpace(parts[1]) kv[k] = v } if err := s.Err(); err != nil { return Sample{}, err } status := kv["ups.status"] if status == "" { return Sample{}, fmt.Errorf("ups.status missing in NUT output") } flags := strings.Fields(status) out := Sample{RawStatus: status} for _, f := range flags { switch strings.ToUpper(f) { case "OB": out.OnBattery = true case "OL": out.OnBattery = false case "LB": out.LowBattery = true } } if runtimeRaw := kv["battery.runtime"]; runtimeRaw != "" { runtime, err := strconv.Atoi(strings.Split(runtimeRaw, ".")[0]) if err == nil { out.RuntimeSeconds = runtime } } return out, nil }