initial commit: geekworm x1208 ups prometheus exporter
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/warthog618/go-gpiocdev"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
i2cDev = "/dev/i2c-1"
|
||||
gpioChip = "gpiochip0"
|
||||
gpioPLDPin = 6
|
||||
maxAddr = 0x36
|
||||
regVoltage = 0x02
|
||||
regCapacity = 0x04
|
||||
i2cSlave = 0x0703 // I2C_SLAVE
|
||||
maxRetries = 3
|
||||
)
|
||||
|
||||
func readI2CWord(fd int, reg byte) (uint16, error) {
|
||||
if _, err := unix.Write(fd, []byte{reg}); err != nil {
|
||||
return 0, fmt.Errorf("write reg: %w", err)
|
||||
}
|
||||
buf := make([]byte, 2)
|
||||
n, err := unix.Read(fd, buf)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("read: %w", err)
|
||||
}
|
||||
if n != 2 {
|
||||
return 0, fmt.Errorf("short read: %d bytes", n)
|
||||
}
|
||||
return binary.BigEndian.Uint16(buf), nil
|
||||
}
|
||||
|
||||
func readI2CWordRetry(fd int, reg byte, validate func(uint16) bool) (uint16, error) {
|
||||
var lastErr error
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
v, err := readI2CWord(fd, reg)
|
||||
if err == nil && validate(v) {
|
||||
return v, nil
|
||||
}
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
} else {
|
||||
lastErr = fmt.Errorf("read out of range: 0x%04x", v)
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
return 0, lastErr
|
||||
}
|
||||
|
||||
func readBattery() (voltage float64, percent float64, err error) {
|
||||
fd, err := unix.Open(i2cDev, unix.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("open %s: %w", i2cDev, err)
|
||||
}
|
||||
defer unix.Close(fd)
|
||||
|
||||
if err := unix.IoctlSetInt(fd, i2cSlave, maxAddr); err != nil {
|
||||
return 0, 0, fmt.Errorf("ioctl I2C_SLAVE: %w", err)
|
||||
}
|
||||
|
||||
rawV, err := readI2CWordRetry(fd, regVoltage, func(v uint16) bool {
|
||||
f := float64(v) * 1.25 / 1000 / 16
|
||||
return f > 2.0 && f < 5.0
|
||||
})
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("voltage: %w", err)
|
||||
}
|
||||
voltage = float64(rawV) * 1.25 / 1000 / 16
|
||||
|
||||
rawC, err := readI2CWordRetry(fd, regCapacity, func(v uint16) bool {
|
||||
return float64(v)/256 <= 110
|
||||
})
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("capacity: %w", err)
|
||||
}
|
||||
percent = float64(rawC) / 256
|
||||
return voltage, percent, nil
|
||||
}
|
||||
|
||||
func readACPower() (int, error) {
|
||||
line, err := gpiocdev.RequestLine(gpioChip, gpioPLDPin,
|
||||
gpiocdev.AsInput,
|
||||
gpiocdev.WithConsumer("x1208-exporter"))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("request gpio: %w", err)
|
||||
}
|
||||
defer line.Close()
|
||||
|
||||
v, err := line.Value()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("read gpio: %w", err)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func writeAtomic(path string, content string) error {
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
tmp, err := os.CreateTemp(dir, ".x1208.*.tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpPath := tmp.Name()
|
||||
defer os.Remove(tmpPath)
|
||||
if _, err := tmp.WriteString(content); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
if err := tmp.Chmod(0o644); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmpPath, path)
|
||||
}
|
||||
|
||||
func main() {
|
||||
out := flag.String("out", "/var/lib/node-exporter/textfiles/x1208.prom", "output prom file")
|
||||
flag.Parse()
|
||||
|
||||
var (
|
||||
ac int
|
||||
voltage float64
|
||||
percent float64
|
||||
acErr, batErr error
|
||||
)
|
||||
|
||||
ac, acErr = readACPower()
|
||||
voltage, percent, batErr = readBattery()
|
||||
|
||||
if acErr != nil && batErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "all reads failed: ac=%v bat=%v\n", acErr, batErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
var content string
|
||||
content += "# HELP rpi_ups_last_update_seconds Unix timestamp of last successful exporter run.\n"
|
||||
content += "# TYPE rpi_ups_last_update_seconds gauge\n"
|
||||
content += fmt.Sprintf("rpi_ups_last_update_seconds %d\n", now)
|
||||
|
||||
if acErr == nil {
|
||||
content += "# HELP rpi_ups_ac_power AC adapter present (1=plugged in, 0=on battery).\n"
|
||||
content += "# TYPE rpi_ups_ac_power gauge\n"
|
||||
content += fmt.Sprintf("rpi_ups_ac_power %d\n", ac)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "ac read failed: %v\n", acErr)
|
||||
}
|
||||
|
||||
if batErr == nil {
|
||||
content += "# HELP rpi_ups_voltage_volts Battery cell voltage from MAX17040 fuel gauge.\n"
|
||||
content += "# TYPE rpi_ups_voltage_volts gauge\n"
|
||||
content += fmt.Sprintf("rpi_ups_voltage_volts %.3f\n", voltage)
|
||||
content += "# HELP rpi_ups_battery_percent Battery state of charge from MAX17040 fuel gauge.\n"
|
||||
content += "# TYPE rpi_ups_battery_percent gauge\n"
|
||||
content += fmt.Sprintf("rpi_ups_battery_percent %.2f\n", percent)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "battery read failed: %v\n", batErr)
|
||||
}
|
||||
|
||||
if err := writeAtomic(*out, content); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "write %s: %v\n", *out, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if acErr != nil || batErr != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user