2021-01-01 16:51:16 +11:00
|
|
|
// The sungrow binary periodically reads inverter data from a sungrow inverter
|
|
|
|
// and exports the data as prometheus metrics.
|
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"flag"
|
|
|
|
"fmt"
|
2021-01-05 20:47:41 +11:00
|
|
|
"log"
|
2021-01-01 16:51:16 +11:00
|
|
|
"net/http"
|
|
|
|
"time"
|
|
|
|
|
2021-01-10 15:14:38 +11:00
|
|
|
"github.com/DrJosh9000/sungrow/modbus"
|
2021-01-01 16:51:16 +11:00
|
|
|
"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")
|
2021-01-04 16:21:29 +11:00
|
|
|
inverterAddr = flag.String("inverter-addr", "rakmodule_00DBC1.local:502", "Address of inverter")
|
2021-01-01 19:50:24 +11:00
|
|
|
scrapeInterval = flag.Duration("scrape-interval", 15*time.Second, "Period of modbus scraping loop")
|
2021-01-01 16:51:16 +11:00
|
|
|
|
|
|
|
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",
|
|
|
|
})
|
2021-01-10 15:20:11 +11:00
|
|
|
scrapeIntervalGauge = promauto.NewGaugeFunc(
|
|
|
|
prometheus.GaugeOpts{
|
|
|
|
Namespace: "sungrow",
|
|
|
|
Subsystem: "scraper",
|
|
|
|
Name: "scrape_interval",
|
|
|
|
Help: "units:s",
|
|
|
|
},
|
|
|
|
func() float64 { return scrapeInterval.Seconds() },
|
|
|
|
)
|
2021-01-10 14:43:46 +11:00
|
|
|
|
|
|
|
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()) },
|
|
|
|
)
|
2021-01-01 16:51:16 +11:00
|
|
|
)
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2021-01-01 19:50:24 +11:00
|
|
|
func readRegs(client modbus.Client, start, qty uint16) {
|
2021-01-01 20:00:07 +11:00
|
|
|
data, err := client.ReadInputRegisters(start, qty)
|
2021-01-01 19:50:24 +11:00
|
|
|
if err != nil {
|
2021-01-05 20:47:41 +11:00
|
|
|
log.Fatalf("Couldn't read input registers %d-%d: %v", start+1, start+qty, err)
|
2021-01-01 19:50:24 +11:00
|
|
|
}
|
|
|
|
for addr, reg := range sungrowInputRegs {
|
|
|
|
if addr <= start || addr > start+qty {
|
|
|
|
continue
|
|
|
|
}
|
2021-01-11 10:21:53 +11:00
|
|
|
val, err := reg.read(data[(addr-start-1)*2:])
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("Couldn't parse input register data: %v", err)
|
|
|
|
}
|
2021-01-01 19:50:24 +11:00
|
|
|
//fmt.Printf("%s: %v %s\n", reg.name, val, reg.unit)
|
|
|
|
registerGauges[addr].Set(val)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-04 10:42:14 +11:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2021-01-01 16:51:16 +11:00
|
|
|
func main() {
|
|
|
|
flag.Parse()
|
|
|
|
|
|
|
|
// Is the inverter reachable?
|
|
|
|
sgc, err := dialSungrow(*inverterAddr)
|
|
|
|
if err != nil {
|
2021-01-05 20:47:41 +11:00
|
|
|
log.Fatalf("Couldn't dial inverter: %v", err)
|
2021-01-01 16:51:16 +11:00
|
|
|
}
|
|
|
|
defer sgc.Close()
|
|
|
|
|
2021-01-04 10:42:14 +11:00
|
|
|
// HTTP setup
|
2021-01-01 16:51:16 +11:00
|
|
|
http.Handle("/metrics", promhttp.Handler())
|
|
|
|
http.HandleFunc("/", statusHandler)
|
|
|
|
|
|
|
|
// Modbus scrape loop
|
2021-01-10 15:14:38 +11:00
|
|
|
handler := modbus.TCPHandlerFromConnection(sgc)
|
2021-01-01 16:51:16 +11:00
|
|
|
handler.SlaveId = 0x01
|
2021-01-10 15:14:38 +11:00
|
|
|
//handler.Connect()
|
2021-01-01 16:51:16 +11:00
|
|
|
defer handler.Close()
|
|
|
|
client := modbus.NewClient(handler)
|
2021-01-04 10:42:14 +11:00
|
|
|
scrape(client)
|
|
|
|
// Start http interface only after first successful scrape
|
2021-01-04 10:43:04 +11:00
|
|
|
go func() {
|
2021-01-05 20:47:41 +11:00
|
|
|
log.Fatalf("http.ListenAndServe: %v", http.ListenAndServe(*httpAddr, nil))
|
2021-01-04 10:43:04 +11:00
|
|
|
}()
|
2021-01-04 10:42:14 +11:00
|
|
|
for range time.Tick(*scrapeInterval) {
|
|
|
|
scrape(client)
|
2021-01-01 16:51:16 +11:00
|
|
|
}
|
|
|
|
}
|