Files
x1208-exporter/main.go
T

182 lines
4.4 KiB
Go

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)
}
}