ina219-exporter/exporter.go

229 lines
6.6 KiB
Go
Raw Normal View History

2024-09-27 11:40:06 +10:00
// 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.
2024-10-18 13:05:25 +11:00
package main // import "drjosh.dev/ina219-exporter"
2024-09-27 11:40:06 +10:00
import (
"flag"
"fmt"
2024-09-27 12:50:13 +10:00
"log"
"net/http"
2024-09-27 11:40:06 +10:00
"os"
"os/signal"
"regexp"
"strconv"
"syscall"
"time"
2024-10-18 13:05:25 +11:00
"drjosh.dev/jmetrics"
2024-09-27 12:50:13 +10:00
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
2024-09-27 11:40:06 +10:00
"periph.io/x/conn/v3/i2c/i2creg"
"periph.io/x/conn/v3/physic"
"periph.io/x/devices/v3/ina219"
"periph.io/x/host/v3"
)
2024-11-06 09:26:14 +11:00
func main() {
2024-09-27 12:50:13 +10:00
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)")
2024-09-27 11:40:06 +10:00
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 {
2024-11-06 09:26:14 +11:00
log.Fatalf("parsing --sense-resistor: %w", err)
2024-09-27 11:40:06 +10:00
}
2024-09-27 11:56:06 +10:00
if resistor <= 0 {
2024-11-06 09:26:14 +11:00
log.Fatalf("--sense-resistor must be positive")
2024-09-27 11:56:06 +10:00
}
2024-09-27 11:40:06 +10:00
current, err := parseNanoSI[physic.ElectricCurrent](*maxCurrent)
if err != nil {
2024-11-06 09:26:14 +11:00
log.Fatalf("parsing --max-current: %w", err)
2024-09-27 11:40:06 +10:00
}
2024-09-27 11:56:06 +10:00
if current <= 0 {
2024-11-06 09:26:14 +11:00
log.Fatalf("--max-current must be positive")
2024-09-27 11:56:06 +10:00
}
2024-09-27 11:40:06 +10:00
if _, err := host.Init(); err != nil {
2024-11-06 09:26:14 +11:00
log.Fatalf("host.Init() error: %v", err)
2024-09-27 11:40:06 +10:00
}
// Open default I²C bus.
2024-09-27 12:50:13 +10:00
bus, err := i2creg.Open(*i2cBus)
2024-09-27 11:40:06 +10:00
if err != nil {
2024-11-06 09:26:14 +11:00
log.Fatalf("i2creg.Open(%v): %v", *i2cBus, err)
2024-09-27 11:40:06 +10:00
}
defer bus.Close()
sensor, err := ina219.New(bus, &ina219.Opts{
2024-09-27 12:50:13 +10:00
Address: *i2cAddress,
2024-09-27 11:40:06 +10:00
SenseResistor: resistor,
MaxCurrent: current,
})
if err != nil {
2024-11-06 09:26:14 +11:00
log.Fatalf("ina219.New(...): %v", err)
2024-09-27 11:40:06 +10:00
}
2024-09-27 13:05:07 +10:00
constLabels := prometheus.Labels{"i2c_addr": fmt.Sprintf("0x%x", *i2cAddress)}
2024-09-27 12:50:13 +10:00
busVoltageHist := promauto.NewHistogram(prometheus.HistogramOpts{
Namespace: "ina219",
Name: "bus_voltage",
Help: "Bus voltage (V)",
ConstLabels: constLabels,
NativeHistogramBucketFactor: 1.001,
NativeHistogramZeroThreshold: 0.001,
})
2024-10-01 14:44:35 +10:00
busVoltageSumm := jmetrics.NewLiteGaugeSummary(jmetrics.LiteGaugeSummaryOpts{
Namespace: "ina219",
Subsystem: "summ",
Name: "bus_voltage",
Help: "Bus voltage (V)",
ConstLabels: constLabels,
})
prometheus.MustRegister(busVoltageSumm)
2024-09-27 12:50:13 +10:00
busCurrentHist := promauto.NewHistogram(prometheus.HistogramOpts{
Namespace: "ina219",
Name: "bus_current",
Help: "Bus current (A)",
ConstLabels: constLabels,
NativeHistogramBucketFactor: 1.001,
NativeHistogramZeroThreshold: 0.001,
})
2024-10-01 14:44:35 +10:00
busCurrentSumm := jmetrics.NewLiteGaugeSummary(jmetrics.LiteGaugeSummaryOpts{
Namespace: "ina219",
Subsystem: "summ",
Name: "bus_current",
Help: "Bus current (A)",
ConstLabels: constLabels,
})
prometheus.MustRegister(busCurrentSumm)
2024-09-27 12:50:13 +10:00
busPowerHist := promauto.NewHistogram(prometheus.HistogramOpts{
Namespace: "ina219",
Name: "bus_power",
Help: "Bus power (W)",
ConstLabels: constLabels,
NativeHistogramBucketFactor: 1.001,
NativeHistogramZeroThreshold: 0.001,
})
2024-10-01 14:44:35 +10:00
busPowerSumm := jmetrics.NewLiteGaugeSummary(jmetrics.LiteGaugeSummaryOpts{
Namespace: "ina219",
Subsystem: "summ",
Name: "bus_power",
Help: "Bus power (W)",
ConstLabels: constLabels,
})
prometheus.MustRegister(busPowerSumm)
2024-09-27 12:50:13 +10:00
shuntVoltageHist := promauto.NewHistogram(prometheus.HistogramOpts{
Namespace: "ina219",
Name: "shunt_voltage",
Help: "Shunt voltage (V)",
ConstLabels: constLabels,
NativeHistogramBucketFactor: 1.001,
NativeHistogramZeroThreshold: 0.001,
})
2024-10-01 14:44:35 +10:00
shuntVoltageSumm := jmetrics.NewLiteGaugeSummary(jmetrics.LiteGaugeSummaryOpts{
Namespace: "ina219",
Subsystem: "summ",
Name: "shunt_voltage",
Help: "Shunt voltage (V)",
ConstLabels: constLabels,
})
prometheus.MustRegister(shuntVoltageSumm)
2024-09-27 12:50:13 +10:00
go func() {
http.Handle("/metrics", promhttp.Handler())
log.Fatal(http.ListenAndServe(*httpAddr, nil))
}()
2024-09-27 11:40:06 +10:00
// Read values from sensor every second.
everySecond := time.NewTicker(time.Second).C
2024-09-27 12:50:13 +10:00
halt := make(chan os.Signal, 1)
2024-09-27 11:40:06 +10:00
signal.Notify(halt, syscall.SIGTERM)
signal.Notify(halt, syscall.SIGINT)
for {
select {
case <-everySecond:
p, err := sensor.Sense()
if err != nil {
2024-11-06 09:26:14 +11:00
log.Fatalf("sensor.Sense() error: %v", err)
2024-09-27 11:40:06 +10:00
}
2024-10-08 16:18:56 +11:00
// 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) {
2024-11-06 09:26:14 +11:00
log.Fatalf("Ohm's Law violation: current = %v but shunt = %v", p.Current, p.Shunt)
2024-10-08 16:18:56 +11:00
}
// power = current * voltage, similar logic
if (p.Power == 0) != (p.Current == 0 || p.Voltage == 0) {
2024-11-06 09:26:14 +11:00
log.Fatalf("Joule's First Law violation: power = %v but current = %v and voltage = %v", p.Power, p.Current, p.Voltage)
2024-10-08 16:18:56 +11:00
}
2024-10-01 14:44:35 +10:00
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)
2024-09-27 12:50:13 +10:00
2024-09-27 11:40:06 +10:00
case <-halt:
2024-11-06 09:26:14 +11:00
return
2024-09-27 11:40:06 +10:00
}
}
}
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:
2024-09-27 11:56:06 +10:00
switch suffix := submatches[2]; suffix {
case "":
value *= 1_000_000_000
2024-09-27 11:40:06 +10:00
case "m":
value *= 1_000_000
case "mu", "µ":
value *= 1_000
case "n":
value *= 1
default:
2024-09-27 11:56:06 +10:00
return 0, fmt.Errorf("invalid suffix %q", suffix)
2024-09-27 11:40:06 +10:00
}
}
return value, nil
}