2023-01-07 16:41:15 +11:00
/ *
Copyright 2023 Josh Deprez
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
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 (
2021-01-12 10:45:52 +11:00
"errors"
2021-01-01 16:51:16 +11:00
"flag"
"fmt"
2021-01-05 20:47:41 +11:00
"log"
2021-01-01 16:51:16 +11:00
"net/http"
2021-02-07 17:52:20 +11:00
"strings"
2023-01-07 17:37:54 +11:00
"sync"
2021-01-01 16:51:16 +11:00
"time"
2023-01-06 16:53:25 +11:00
"gitea.drjosh.dev/josh/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"
)
2023-01-07 17:37:54 +11:00
const maxScrapeAge = 3 * time . Second
2021-01-01 16:51:16 +11:00
var (
httpAddr = flag . String ( "http-addr" , ":9455" , "Address to listen on" )
2021-02-07 17:52:20 +11:00
inverterAddrs = flag . String ( "inverter-addrs" , "rakmodule_00DBC1:502,192.168.86.6:502" , "Comma-separated list of inverter addresses (modbus-tcp with 'encryption')" )
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
2023-01-07 17:37:54 +11:00
scrapeMu sync . Mutex
lastScrape time . Time
lastValues = make ( map [ uint16 ] float64 )
2021-01-01 16:51:16 +11:00
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" ,
} )
2023-01-07 16:41:15 +11:00
)
func init ( ) {
promauto . NewGaugeFunc (
2021-01-10 15:20:11 +11:00
prometheus . GaugeOpts {
Namespace : "sungrow" ,
Subsystem : "scraper" ,
Name : "scrape_interval" ,
Help : "units:s" ,
} ,
func ( ) float64 { return scrapeInterval . Seconds ( ) } ,
)
2023-01-07 16:41:15 +11:00
promauto . NewGaugeFunc (
2021-01-10 14:43:46 +11:00
prometheus . GaugeOpts {
Namespace : "sungrow" ,
Subsystem : "tariff" ,
Name : "daily_charge" ,
Help : "unit:$" ,
} ,
func ( ) float64 { return dailySupplyCharge } ,
)
2023-01-07 16:41:15 +11:00
promauto . NewGaugeFunc (
2021-01-10 14:43:46 +11:00
prometheus . GaugeOpts {
Namespace : "sungrow" ,
Subsystem : "tariff" ,
Name : "import_tariff" ,
Help : "unit:$" ,
} ,
func ( ) float64 { return tariff93 . pricePerKWh ( time . Now ( ) ) } ,
)
2023-01-07 16:41:15 +11:00
promauto . NewGaugeFunc (
2021-01-10 14:43:46 +11:00
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 statusHandler ( w http . ResponseWriter , r * http . Request ) {
2021-01-19 15:11:26 +11:00
fmt . Fprintf ( w , "current time: %v\n" , time . Now ( ) )
2021-01-01 16:51:16 +11:00
}
2023-01-07 17:37:54 +11:00
// Called under scrapeMu.
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
}
2021-02-02 16:25:58 +11:00
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 )
}
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 {
2021-01-12 10:45:52 +11:00
if errors . Is ( err , errSkippableRead ) {
2021-02-03 21:28:24 +11:00
log . Printf ( "Couldn't parse input register data at %d, skipping: %v" , addr , err )
2021-01-12 10:45:52 +11:00
continue
}
2021-02-03 21:28:24 +11:00
log . Fatalf ( "Couldn't parse input register data at %d: %v" , addr , err )
2021-01-11 10:21:53 +11:00
}
2021-01-01 19:50:24 +11:00
//fmt.Printf("%s: %v %s\n", reg.name, val, reg.unit)
2023-01-07 17:37:54 +11:00
lastValues [ addr ] = val
2021-01-01 19:50:24 +11:00
}
}
2023-01-07 17:37:54 +11:00
// Called under scrapeMu.
2021-01-04 10:42:14 +11:00
func scrape ( client modbus . Client ) {
start := time . Now ( )
scrapeStart . SetToCurrentTime ( )
2021-01-25 16:05:57 +11:00
readRegs ( client , 5000 , 100 )
2021-01-04 10:42:14 +11:00
readRegs ( client , 5100 , 50 )
scrapeEnd . SetToCurrentTime ( )
2023-01-07 17:37:54 +11:00
lastScrape = time . Now ( )
2021-01-04 10:42:14 +11:00
scrapeDuration . Set ( time . Since ( start ) . Seconds ( ) )
scrapeCounter . Inc ( )
}
2021-01-01 16:51:16 +11:00
func main ( ) {
flag . Parse ( )
// Is the inverter reachable?
2021-02-07 17:53:56 +11:00
var sgc * sungrowConn
2021-02-07 17:52:20 +11:00
for _ , addr := range strings . Split ( * inverterAddrs , "," ) {
2021-02-07 17:53:56 +11:00
conn , err := dialSungrow ( addr )
2021-02-07 17:52:20 +11:00
if err != nil {
2021-02-07 17:53:56 +11:00
log . Printf ( "Couldn't dial inverter: %v" , err )
2021-02-07 18:01:27 +11:00
continue
2021-02-07 17:52:20 +11:00
}
2021-02-07 17:53:56 +11:00
sgc = conn
defer conn . Close ( )
2021-02-07 17:52:20 +11:00
break
2021-01-01 16:51:16 +11:00
}
2021-02-07 17:53:56 +11:00
if sgc == nil {
log . Fatal ( "Couldn't dial any addresses, aborting" )
}
2021-01-01 16:51:16 +11:00
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 )
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 )
2023-01-07 17:37:54 +11:00
// 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 {
2023-01-07 17:49:22 +11:00
addr , reg := addr , reg
2023-01-07 17:37:54 +11:00
promauto . NewGaugeFunc (
prometheus . GaugeOpts {
Namespace : "sungrow" ,
Subsystem : "inverter" ,
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 ]
} ,
)
2021-01-01 16:51:16 +11:00
}
2023-01-07 17:37:54 +11:00
log . Fatalf ( "http.ListenAndServe: %v" , http . ListenAndServe ( * httpAddr , nil ) )
2021-01-01 16:51:16 +11:00
}