// The sungrow binary periodically reads inverter data from a sungrow inverter // and exports the data as prometheus metrics. package main import ( "errors" "flag" "fmt" "log" "net/http" "strings" "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") inverterAddrs = flag.String("inverter-addrs", "rakmodule_00DBC1:502,192.168.86.6:502", "Comma-separated list of inverter addresses (modbus-tcp with 'encryption')") 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) { fmt.Fprintf(w, "current time: %v\n", time.Now()) } 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) } if len(data) != int(2*qty) { log.Fatalf("Couldn't read input registers %d-%d: len(data) = %d != %d = 2*qty", start+1, start+qty, len(data), 2*qty) } for addr, reg := range sungrowInputRegs { if addr <= start || addr > start+qty { continue } val, err := reg.read(data[(addr-start-1)*2:]) if err != nil { if errors.Is(err, errSkippableRead) { log.Printf("Couldn't parse input register data at %d, skipping: %v", addr, err) continue } log.Fatalf("Couldn't parse input register data at %d: %v", addr, 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, 100) readRegs(client, 5100, 50) scrapeEnd.SetToCurrentTime() scrapeDuration.Set(time.Since(start).Seconds()) scrapeCounter.Inc() } func main() { flag.Parse() // Is the inverter reachable? var sgc *sungrowConn for _, addr := range strings.Split(*inverterAddrs, ",") { conn, err := dialSungrow(addr) if err != nil { log.Printf("Couldn't dial inverter: %v", err) continue } sgc = conn defer conn.Close() break } if sgc == nil { log.Fatal("Couldn't dial any addresses, aborting") } // 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) } }