228 lines
6.6 KiB
Go
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
|
|
}
|