167 lines
5.3 KiB
Go
167 lines
5.3 KiB
Go
// The MIT License (MIT)
|
|
// Copyright © 2022 <Tom Demeyer>
|
|
// 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)
|
|
}
|
|
}
|
|
}
|