// The sungrow binary periodically reads inverter data from a sungrow inverter // and exports the data as prometheus metrics. package main import ( "flag" "fmt" "log" "net/http" "time" "github.com/DrJosh9000/sungrow/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.NewGaugeFunc( prometheus.GaugeOpts{ Namespace: "sungrow", Subsystem: "scraper", Name: "scrape_interval", Help: "units:s", }, func() float64 { return scrapeInterval.Seconds() }, ) dailyChargeGauge = promauto.NewGaugeFunc( prometheus.GaugeOpts{ Namespace: "sungrow", Subsystem: "tariff", Name: "daily_charge", Help: "unit:$", }, func() float64 { return dailySupplyCharge }, ) importTariffGauge = promauto.NewGaugeFunc( prometheus.GaugeOpts{ Namespace: "sungrow", Subsystem: "tariff", Name: "import_tariff", Help: "unit:$", }, func() float64 { return tariff93.pricePerKWh(time.Now()) }, ) exportTariffGauge = promauto.NewGaugeFunc( prometheus.GaugeOpts{ Namespace: "sungrow", Subsystem: "tariff", Name: "export_tariff", Help: "unit:$", }, func() float64 { return solarFeedInTariff.pricePerKWh(time.Now()) }, ) ) 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, err := reg.read(data[(addr-start-1)*2:]) if err != nil { log.Fatalf("Couldn't parse input register data: %v", err) } //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() // Is the inverter reachable? sgc, err := dialSungrow(*inverterAddr) if err != nil { log.Fatalf("Couldn't dial inverter: %v", err) } defer sgc.Close() // HTTP setup http.Handle("/metrics", promhttp.Handler()) http.HandleFunc("/", statusHandler) // Modbus scrape loop handler := modbus.TCPHandlerFromConnection(sgc) 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) } }