// The MIT License (MIT) // Copyright © 2022 // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and // associated documentation files (the “Software”), to deal in the Software without restriction, // including without limitation the rights to use, copy, modify, merge, publish, distribute, // sublicense, and/or sell copies of the Software, and to permit persons to whom the Software // is furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // This is a libux service that periodically queries the Solaredge API server and dumps the data into // an influx database (that needs to be reachable and available without authentication). // Typical setup is a NAS or Raspberry Pi that runs both this service, the influx database and grafana package main import ( "bytes" "flag" "fmt" "io" "log" "net/http" "os" "os/signal" "strings" "syscall" "time" "github.com/Jeffail/gabs" "github.com/davecgh/go-spew/spew" "github.com/robfig/cron/v3" ) var cetLoc *time.Location var jobs *cron.Cron const dtFormat string = "2006-01-02 15:04:05" func update(retry uint8, ue string) { now := time.Now() t_e := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location()) t_s := time.Date(now.Year(), now.Month(), now.Day(), now.Hour()-1, 0, 0, 0, now.Location()) eurl := strings.Replace(fmt.Sprintf(ue, t_s.Format(dtFormat), t_e.Format(dtFormat)), " ", "%20", -1) if *xverbose { log.Printf("GET %s\n", eurl) } eresp, err := http.Get(eurl) if err != nil { log.Println(err) return } defer eresp.Body.Close() // api calls need to be paced consierably to prevent being locked out. if eresp.StatusCode != 200 && retry < 5 { log.Printf("status = %s, retrying...\n", eresp.Status) time.Sleep(5 * time.Minute) update(retry+1, ue) return } if eresp.StatusCode == 200 { if *verbose { log.Printf("got data from %v to %v\n", t_s, t_e) } body, err := io.ReadAll(eresp.Body) if err != nil { log.Println(err) return } update_equipment(body) } else { log.Println(eresp.Status) } } func update_equipment(body []byte) { jsonParsed, err := gabs.ParseJSON(body) if err != nil { log.Println(err) log.Printf("error: %v\nbody:\n%s\n", err, string(body)) return } children, err := jsonParsed.S("data", "telemetries").Children() if err != nil { log.Println(err) return } for _, child := range children { var dcVoltage float64 timeT, _ := time.ParseInLocation(dtFormat, child.S("date").Data().(string), cetLoc) if v, ok := child.S("dcVoltage").Data().(float64); ok { dcVoltage = float64(v) } else { dcVoltage = 0.0 } acCurrent := child.S("L1Data", "acCurrent").Data().(float64) acVoltage := child.S("L1Data", "acVoltage").Data().(float64) acFrequency := child.S("L1Data", "acFrequency").Data().(float64) activePower := child.S("L1Data", "activePower").Data().(float64) totalEnergy := child.S("totalEnergy").Data().(float64) temperature := child.S("temperature").Data().(float64) s := fmt.Sprintf("solar dcVoltage=%0.2f,acCurrent=%0.2f,acVoltage=%0.2f,acFrequency=%0.2f,activePower=%0.2f,totalEnergy=%0.2f,temperature=%0.2f %d", dcVoltage, acCurrent, acVoltage, acFrequency, activePower, totalEnergy, temperature, timeT.UnixNano()) if *xverbose { log.Printf("update_en: %s\n", s) } updateInflux(s) } } // INFLUX_SERVER format: http://192.168.178.8:8086 var ifhost = flag.String("influx", os.Getenv("INFLUX_SERVER"), "the http write url of the influx db; includes port if not 80.") var verbose = flag.Bool("v", false, "log events.") var xverbose = flag.Bool("vv", false, "log more events.") func updateInflux(ifData string) { if ifData != "" { respi, err := http.Post(*ifhost+"/write?db=solaredge", "text/plain", bytes.NewBufferString(ifData)) if err != nil { fmt.Fprintf(os.Stderr, "Error post to influx: %v\n", err) } else { if *xverbose { fmt.Fprintf(os.Stderr, "influx write: %s\n", ifData) } respi.Body.Close() } } } func dumpStatus() { for j := range jobs.Entries() { spew.Dump(j) } } func main() { flag.Parse() api_url := "https://monitoringapi.solaredge.com/equipment/2353133/74037025-0C/data?api_key=" + os.Getenv("SE_API_KEY") + "&startTime=%s&endTime=%s" cetLoc, _ = time.LoadLocation("Europe/Amsterdam") jobs = cron.New() // every hour on the 25th minute // api calls need to be paced consierably to prevent being locked out. jobs.AddFunc("25 * * * *", func() { update(0, api_url) }) jobs.Start() fmt.Printf("solaredge API query service started; q every hour at minute 25.\n") sig := make(chan os.Signal, 4) signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM) for { s := <-sig switch s { case syscall.SIGHUP: dumpStatus() default: log.Printf("\x1b[2KSignal: %v\n", s) os.Exit(0) } } }