diff --git a/main.go b/main.go index fa58af6..ffccf2b 100644 --- a/main.go +++ b/main.go @@ -41,24 +41,28 @@ var ( 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") - scrapeMu sync.Mutex - lastScrape time.Time - lastValues = make(map[uint16]float64) + promHandler = promhttp.Handler() + scrapeMu sync.Mutex + lastScrape time.Time + lastValues = make(map[uint16]float64) scrapeCounter = promauto.NewCounter(prometheus.CounterOpts{ Namespace: "sungrow", Subsystem: "scraper", Name: "scrapes_total", + Help: "Number of successful scrapes of the inverter input registers", }) scrapeStart = promauto.NewGauge(prometheus.GaugeOpts{ Namespace: "sungrow", Subsystem: "scraper", Name: "scrape_start", + Help: "Start time of the most recent scrape attempt", }) scrapeEnd = promauto.NewGauge(prometheus.GaugeOpts{ Namespace: "sungrow", Subsystem: "scraper", Name: "scrape_end", + Help: "End time of the most recent successful scrape", }) scrapeDuration = promauto.NewGauge(prometheus.GaugeOpts{ Namespace: "sungrow", @@ -83,7 +87,7 @@ func init() { Namespace: "sungrow", Subsystem: "tariff", Name: "daily_charge", - Help: "unit:$", + Help: "units:$", }, func() float64 { return dailySupplyCharge }, ) @@ -92,7 +96,7 @@ func init() { Namespace: "sungrow", Subsystem: "tariff", Name: "import_tariff", - Help: "unit:$", + Help: "units:$", }, func() float64 { return tariff93.pricePerKWh(time.Now()) }, ) @@ -101,7 +105,7 @@ func init() { Namespace: "sungrow", Subsystem: "tariff", Name: "export_tariff", - Help: "unit:$", + Help: "units:$", }, func() float64 { return solarFeedInTariff.pricePerKWh(time.Now()) }, ) @@ -111,14 +115,26 @@ func statusHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "current time: %v\n", time.Now()) } +func dialInverter() (*sungrowConn, error) { + for _, addr := range strings.Split(*inverterAddrs, ",") { + conn, err := dialSungrow(addr) + if err != nil { + log.Printf("Couldn't dial inverter: %v", err) + continue + } + return conn, nil + } + return nil, fmt.Errorf("all addresses unreachable") +} + // Called under scrapeMu. -func readRegs(client modbus.Client, start, qty uint16) { +func readRegs(client modbus.Client, start, qty uint16) error { data, err := client.ReadInputRegisters(start, qty) if err != nil { - log.Fatalf("Couldn't read input registers %d-%d: %v", start+1, start+qty, err) + return fmt.Errorf("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) + return fmt.Errorf("reading 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 { @@ -130,56 +146,72 @@ func readRegs(client modbus.Client, start, qty uint16) { 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) + return fmt.Errorf("parsing input register data at %d: %v", addr, err) } //fmt.Printf("%s: %v %s\n", reg.name, val, reg.unit) lastValues[addr] = val } + return nil } // Called under scrapeMu. -func scrape(client modbus.Client) { +func scrape() error { + sgc, err := dialInverter() + if err != nil { + return err + } + defer sgc.Close() + + handler := modbus.TCPHandlerFromConnection(sgc) + handler.SlaveId = 0x01 + //handler.Connect() + + client := modbus.NewClient(handler) + start := time.Now() scrapeStart.SetToCurrentTime() - readRegs(client, 5000, 50) - readRegs(client, 5050, 50) - readRegs(client, 5100, 50) + if err := readRegs(client, 5000, 50); err != nil { + return err + } + if err := readRegs(client, 5050, 50); err != nil { + return err + } + if err := readRegs(client, 5100, 50); err != nil { + return err + } scrapeEnd.SetToCurrentTime() lastScrape = time.Now() scrapeDuration.Set(time.Since(start).Seconds()) scrapeCounter.Inc() + + return nil +} + +func metricsHandler(w http.ResponseWriter, r *http.Request) { + // In normal mode, always serve metrics + defer promHandler.ServeHTTP(w, r) + + scrapeMu.Lock() + defer scrapeMu.Unlock() + if time.Since(lastScrape) <= maxScrapeAge { + return + } + + var lastErr error + for i := 0; i < 3; i++ { + if err := scrape(); err != nil { + log.Printf("Retrying scrape; error: %v", err) + lastErr = err + continue + } + return + } + log.Fatalf("Scrape failed, bailing entirely: %v", lastErr) } 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) - - handler := modbus.TCPHandlerFromConnection(sgc) - handler.SlaveId = 0x01 - //handler.Connect() - defer handler.Close() - client := modbus.NewClient(handler) - - // Finally, create all the register gauges. // These are GaugeFuncs to more closely align Prometheus scrape time with // the modbus scrape time. for addr, reg := range sungrowInputRegs { @@ -191,17 +223,20 @@ func main() { Name: reg.name, Help: fmt.Sprintf("addr: %d, unit: %s", addr, reg.unit), }, - func() float64 { - scrapeMu.Lock() - defer scrapeMu.Unlock() - if time.Since(lastScrape) <= maxScrapeAge { - return lastValues[addr] - } - scrape(client) - return lastValues[addr] - }, + func() float64 { return lastValues[addr] }, ) } + // Startup paranoia check: Is the inverter reachable? + sgc, err := dialInverter() + if err != nil { + log.Fatal("Couldn't dial any addresses, aborting") + } + sgc.Close() + + // HTTP setup + http.HandleFunc("/metrics", metricsHandler) + http.HandleFunc("/", statusHandler) + log.Fatalf("http.ListenAndServe: %v", http.ListenAndServe(*httpAddr, nil)) }