sungrow/main.go
2021-01-11 10:21:53 +11:00

156 lines
4.1 KiB
Go

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