ina219-exporter/exporter.go
2024-11-06 09:26:24 +11:00

228 lines
6.6 KiB
Go

// Adapted from https://github.com/periph/cmd/blob/main/ina219/main.go
//
// Copyright 2018 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
package main // import "drjosh.dev/ina219-exporter"
import (
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"regexp"
"strconv"
"syscall"
"time"
"drjosh.dev/jmetrics"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
"periph.io/x/conn/v3/i2c/i2creg"
"periph.io/x/conn/v3/physic"
"periph.io/x/devices/v3/ina219"
"periph.io/x/host/v3"
)
func main() {
httpAddr := flag.String("http-address", ":9455", "Listen addr for HTTP handler")
i2cAddress := flag.Int("i2c-address", 0x45, "I²C address")
i2cBus := flag.String("i2c-bus", "", "I²C bus (/dev/i2c-1)")
senseResistor := flag.String("sense-resistor", "10m", "Resistance of the shunt resistor in ohms (optionally with suffix m = milli, {µ,mu} = micro, n = nano)")
maxCurrent := flag.String("max-current", "8", "Maximum current through the device in amps (optional suffix as for --sense-resistor)")
flag.Parse()
resistor, err := parseNanoSI[physic.ElectricResistance](*senseResistor)
if err != nil {
log.Fatalf("parsing --sense-resistor: %w", err)
}
if resistor <= 0 {
log.Fatalf("--sense-resistor must be positive")
}
current, err := parseNanoSI[physic.ElectricCurrent](*maxCurrent)
if err != nil {
log.Fatalf("parsing --max-current: %w", err)
}
if current <= 0 {
log.Fatalf("--max-current must be positive")
}
if _, err := host.Init(); err != nil {
log.Fatalf("host.Init() error: %v", err)
}
// Open default I²C bus.
bus, err := i2creg.Open(*i2cBus)
if err != nil {
log.Fatalf("i2creg.Open(%v): %v", *i2cBus, err)
}
defer bus.Close()
sensor, err := ina219.New(bus, &ina219.Opts{
Address: *i2cAddress,
SenseResistor: resistor,
MaxCurrent: current,
})
if err != nil {
log.Fatalf("ina219.New(...): %v", err)
}
constLabels := prometheus.Labels{"i2c_addr": fmt.Sprintf("0x%x", *i2cAddress)}
busVoltageHist := promauto.NewHistogram(prometheus.HistogramOpts{
Namespace: "ina219",
Name: "bus_voltage",
Help: "Bus voltage (V)",
ConstLabels: constLabels,
NativeHistogramBucketFactor: 1.001,
NativeHistogramZeroThreshold: 0.001,
})
busVoltageSumm := jmetrics.NewLiteGaugeSummary(jmetrics.LiteGaugeSummaryOpts{
Namespace: "ina219",
Subsystem: "summ",
Name: "bus_voltage",
Help: "Bus voltage (V)",
ConstLabels: constLabels,
})
prometheus.MustRegister(busVoltageSumm)
busCurrentHist := promauto.NewHistogram(prometheus.HistogramOpts{
Namespace: "ina219",
Name: "bus_current",
Help: "Bus current (A)",
ConstLabels: constLabels,
NativeHistogramBucketFactor: 1.001,
NativeHistogramZeroThreshold: 0.001,
})
busCurrentSumm := jmetrics.NewLiteGaugeSummary(jmetrics.LiteGaugeSummaryOpts{
Namespace: "ina219",
Subsystem: "summ",
Name: "bus_current",
Help: "Bus current (A)",
ConstLabels: constLabels,
})
prometheus.MustRegister(busCurrentSumm)
busPowerHist := promauto.NewHistogram(prometheus.HistogramOpts{
Namespace: "ina219",
Name: "bus_power",
Help: "Bus power (W)",
ConstLabels: constLabels,
NativeHistogramBucketFactor: 1.001,
NativeHistogramZeroThreshold: 0.001,
})
busPowerSumm := jmetrics.NewLiteGaugeSummary(jmetrics.LiteGaugeSummaryOpts{
Namespace: "ina219",
Subsystem: "summ",
Name: "bus_power",
Help: "Bus power (W)",
ConstLabels: constLabels,
})
prometheus.MustRegister(busPowerSumm)
shuntVoltageHist := promauto.NewHistogram(prometheus.HistogramOpts{
Namespace: "ina219",
Name: "shunt_voltage",
Help: "Shunt voltage (V)",
ConstLabels: constLabels,
NativeHistogramBucketFactor: 1.001,
NativeHistogramZeroThreshold: 0.001,
})
shuntVoltageSumm := jmetrics.NewLiteGaugeSummary(jmetrics.LiteGaugeSummaryOpts{
Namespace: "ina219",
Subsystem: "summ",
Name: "shunt_voltage",
Help: "Shunt voltage (V)",
ConstLabels: constLabels,
})
prometheus.MustRegister(shuntVoltageSumm)
go func() {
http.Handle("/metrics", promhttp.Handler())
log.Fatal(http.ListenAndServe(*httpAddr, nil))
}()
// Read values from sensor every second.
everySecond := time.NewTicker(time.Second).C
halt := make(chan os.Signal, 1)
signal.Notify(halt, syscall.SIGTERM)
signal.Notify(halt, syscall.SIGINT)
for {
select {
case <-everySecond:
p, err := sensor.Sense()
if err != nil {
log.Fatalf("sensor.Sense() error: %v", err)
}
// sanity check sensor outputs:
// current = shunt / resistor, but resistor is fixed,
// so if current == 0 if and only if shunt == 0
if (p.Current == 0) != (p.Shunt == 0) {
log.Fatalf("Ohm's Law violation: current = %v but shunt = %v", p.Current, p.Shunt)
}
// power = current * voltage, similar logic
if (p.Power == 0) != (p.Current == 0 || p.Voltage == 0) {
log.Fatalf("Joule's First Law violation: power = %v but current = %v and voltage = %v", p.Power, p.Current, p.Voltage)
}
busVolts := float64(p.Voltage) / float64(physic.Volt)
busVoltageHist.Observe(busVolts)
busVoltageSumm.Observe(busVolts)
busAmps := float64(p.Current) / float64(physic.Ampere)
busCurrentHist.Observe(busAmps)
busCurrentSumm.Observe(busAmps)
busWatts := float64(p.Power) / float64(physic.Watt)
busPowerHist.Observe(busWatts)
busPowerSumm.Observe(busWatts)
shuntVolts := float64(p.Shunt) / float64(physic.Volt)
shuntVoltageHist.Observe(shuntVolts)
shuntVoltageSumm.Observe(shuntVolts)
case <-halt:
return
}
}
}
var siSmallRE = regexp.MustCompile(`^(\d+)(m|mu|µ|n)?$`)
func parseNanoSI[U ~int64](s string) (U, error) {
submatches := siSmallRE.FindStringSubmatch(s)
if len(submatches) < 2 {
return 0, fmt.Errorf("value %q did not match %q", s, siSmallRE)
}
valueInt, err := strconv.Atoi(submatches[1])
if err != nil {
return 0, err
}
value := U(valueInt)
switch len(submatches) {
case 2:
value *= 1_000_000_000
case 3:
switch suffix := submatches[2]; suffix {
case "":
value *= 1_000_000_000
case "m":
value *= 1_000_000
case "mu", "µ":
value *= 1_000
case "n":
value *= 1
default:
return 0, fmt.Errorf("invalid suffix %q", suffix)
}
}
return value, nil
}