sungrow/main.go
Josh Deprez 11fae1e914 argh
2021-02-07 18:01:27 +11:00

174 lines
4.6 KiB
Go

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