| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package apcupsd |
| 4 | |
| 5 | import ( |
| 6 | "bufio" |
| 7 | "bytes" |
| 8 | "fmt" |
| 9 | "strconv" |
| 10 | "strings" |
| 11 | ) |
| 12 | |
| 13 | var upsStatuses = []string{ |
| 14 | "CAL", |
| 15 | "TRIM", |
| 16 | "BOOST", |
| 17 | "ONLINE", |
| 18 | "ONBATT", |
| 19 | "OVERLOAD", |
| 20 | "LOWBATT", |
| 21 | "REPLACEBATT", |
| 22 | "NOBATT", |
| 23 | "SLAVE", |
| 24 | "SLAVEDOWN", |
| 25 | "COMMLOST", |
| 26 | "SHUTTING_DOWN", |
| 27 | } |
| 28 | |
| 29 | var upsSelftestStatuses = []string{ |
| 30 | "NO", |
| 31 | "NG", |
| 32 | "WN", |
| 33 | "IP", |
| 34 | "OK", |
| 35 | "BT", |
| 36 | "UNK", |
| 37 | } |
| 38 | |
| 39 | // examples: https://github.com/therealbstern/apcupsd/tree/master/examples/status |
| 40 | type apcupsdStatus struct { |
| 41 | bcharge *float64 // battery charge level (percentage) |
| 42 | battv *float64 // battery voltage (Volts) |
| 43 | nombattv *float64 // nominal battery voltage (Volts) |
| 44 | linev *float64 // line voltage (Volts) |
| 45 | minlinev *float64 // min line voltage (Volts) |
| 46 | maxlinev *float64 // max line voltage (Volts) |
| 47 | linefreq *float64 // line frequency (Hz) |
| 48 | outputv *float64 // output voltage (Volts) |
| 49 | nomoutv *float64 // nominal output voltage (Volts) |
| 50 | loadpct *float64 // UPS Load (Percent Load Capacity) |
| 51 | itemp *float64 // internal UPS temperature (Celsius) |
| 52 | nompower *float64 // nominal power (Watts) |
| 53 | timeleft *float64 // estimated runtime left (minutes) |
| 54 | battdate string // Last battery change date (MM/DD/YY or YYYY-MM-DD) |
| 55 | status string |
| 56 | selftest string |
| 57 | } |
| 58 | |
| 59 | func parseStatus(resp []byte) (*apcupsdStatus, error) { |
| 60 | var st apcupsdStatus |
| 61 | sc := bufio.NewScanner(bytes.NewBuffer(resp)) |
| 62 | |
| 63 | for sc.Scan() { |
| 64 | line := sc.Text() |
| 65 | |
| 66 | key, value, ok := strings.Cut(line, ":") |
| 67 | if !ok { |
| 68 | continue |
| 69 | } |
| 70 | |
| 71 | key, value = strings.TrimSpace(key), strings.TrimSpace(value) |
| 72 | |
| 73 | if value == "N/A" { |
| 74 | continue |
| 75 | } |
| 76 | |
| 77 | var err error |
| 78 | |
| 79 | // https://github.com/therealbstern/apcupsd/blob/224d19d5faa508d04267f6135fe53d50800550de/src/lib/apcstatus.c#L30 |
| 80 | switch key { |
| 81 | case "BCHARGE": |
| 82 | st.bcharge, err = parseFloat(value) |
| 83 | case "BATTV": |
| 84 | st.battv, err = parseFloat(value) |
| 85 | case "NOMBATTV": |
| 86 | st.nombattv, err = parseFloat(value) |
| 87 | case "LINEV": |
| 88 | st.linev, err = parseFloat(value) |
| 89 | case "MINLINEV": |
| 90 | st.minlinev, err = parseFloat(value) |
| 91 | case "MAXLINEV": |
| 92 | st.maxlinev, err = parseFloat(value) |
| 93 | case "LINEFREQ": |
| 94 | st.linefreq, err = parseFloat(value) |
| 95 | case "OUTPUTV": |
| 96 | st.outputv, err = parseFloat(value) |
| 97 | case "NOMOUTV": |
| 98 | st.nomoutv, err = parseFloat(value) |
| 99 | case "LOADPCT": |
| 100 | st.loadpct, err = parseFloat(value) |
| 101 | case "ITEMP": |
| 102 | st.itemp, err = parseFloat(value) |
| 103 | case "NOMPOWER": |
| 104 | st.nompower, err = parseFloat(value) |
| 105 | case "TIMELEFT": |
| 106 | st.timeleft, err = parseFloat(value) |
| 107 | case "BATTDATE": |
| 108 | st.battdate = value |
| 109 | case "STATUS": |
| 110 | if value == "SHUTTING DOWN" { |
| 111 | value = "SHUTTING_DOWN" |
| 112 | } |
| 113 | st.status = value |
| 114 | case "SELFTEST": |
| 115 | if value == "??" { |
| 116 | value = "UNK" |
| 117 | } |
| 118 | st.selftest = value |
| 119 | default: |
| 120 | continue |
| 121 | } |
| 122 | if err != nil { |
| 123 | return nil, fmt.Errorf("line '%s': %v", line, err) |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | return &st, nil |
| 128 | } |
| 129 | |
| 130 | func parseFloat(s string) (*float64, error) { |
| 131 | val, _, _ := strings.Cut(s, " ") |
| 132 | f, err := strconv.ParseFloat(val, 64) |
| 133 | if err != nil { |
| 134 | return nil, err |
| 135 | } |
| 136 | return &f, nil |
| 137 | } |