sungrow/main.go
2021-01-05 20:47:41 +11:00

143 lines
3.8 KiB
Go

// The sungrow binary periodically reads inverter data from a sungrow inverter
// and exports the data as prometheus metrics.
package main
import (
"flag"
"fmt"
"io"
"log"
"net"
"net/http"
"time"
"github.com/goburrow/modbus"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
httpAddr = flag.String("http-addr", ":9455", "Address to listen on")
inverterAddr = flag.String("inverter-addr", "rakmodule_00DBC1.local:502", "Address of inverter")
scrapeInterval = flag.Duration("scrape-interval", 15*time.Second, "Period of modbus scraping loop")
registerGauges = make(map[uint16]prometheus.Gauge)
scrapeCounter = promauto.NewCounter(prometheus.CounterOpts{
Namespace: "sungrow",
Subsystem: "scraper",
Name: "scrapes_total",
})
scrapeStart = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "sungrow",
Subsystem: "scraper",
Name: "scrape_start",
})
scrapeEnd = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "sungrow",
Subsystem: "scraper",
Name: "scrape_end",
})
scrapeDuration = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "sungrow",
Subsystem: "scraper",
Name: "scrape_duration",
Help: "units:s",
})
scrapeIntervalGauge = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "sungrow",
Subsystem: "scraper",
Name: "scrape_interval",
Help: "units:s",
})
)
func init() {
for addr, reg := range sungrowInputRegs {
registerGauges[addr] = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "sungrow",
Subsystem: "inverter",
Name: reg.name,
Help: fmt.Sprintf("addr: %d, unit: %s", addr, reg.unit),
})
}
}
func statusHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "not implemented", http.StatusNotFound)
}
func readRegs(client modbus.Client, start, qty uint16) {
data, err := client.ReadInputRegisters(start, qty)
if err != nil {
log.Fatalf("Couldn't read input registers %d-%d: %v", start+1, start+qty, err)
}
for addr, reg := range sungrowInputRegs {
if addr <= start || addr > start+qty {
continue
}
val := reg.read(data[(addr-start-1)*2:])
//fmt.Printf("%s: %v %s\n", reg.name, val, reg.unit)
registerGauges[addr].Set(val)
}
}
func scrape(client modbus.Client) {
start := time.Now()
scrapeStart.SetToCurrentTime()
readRegs(client, 5000, 50)
readRegs(client, 5100, 50)
scrapeEnd.SetToCurrentTime()
scrapeDuration.Set(time.Since(start).Seconds())
scrapeCounter.Inc()
}
func main() {
flag.Parse()
scrapeIntervalGauge.Set(float64(*scrapeInterval))
// Is the inverter reachable?
sgc, err := dialSungrow(*inverterAddr)
if err != nil {
log.Fatalf("Couldn't dial inverter: %v", err)
}
defer sgc.Close()
// the modbus package doesn't provide a client type that can use an
// established connection. quick hack: expose a normal-looking modbus server
// to connect to, and connect to it immediately.
ln, err := net.Listen("tcp", "localhost:0")
if err != nil {
log.Fatalf("Couldn't open listener: %v", err)
}
defer ln.Close()
go func() {
// Only one connection needed!
conn, err := ln.Accept()
if err != nil {
log.Fatalf("Couldn't accept connection: %v", err)
}
go io.Copy(sgc, conn)
io.Copy(conn, sgc)
}()
// HTTP setup
http.Handle("/metrics", promhttp.Handler())
http.HandleFunc("/", statusHandler)
// Modbus scrape loop
handler := modbus.NewTCPClientHandler(ln.Addr().String())
handler.SlaveId = 0x01
handler.Connect()
defer handler.Close()
client := modbus.NewClient(handler)
scrape(client)
// Start http interface only after first successful scrape
go func() {
log.Fatalf("http.ListenAndServe: %v", http.ListenAndServe(*httpAddr, nil))
}()
for range time.Tick(*scrapeInterval) {
scrape(client)
}
}