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