hems/solaredge/solar.go
2024-05-18 14:14:49 +02:00

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