shelly-proxy/proxy.go
2023-12-16 14:34:23 +01:00

258 lines
7.1 KiB
Go

package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/peterbourgon/ff/v3"
"github.com/robfig/cron/v3"
)
var (
flagset = flag.NewFlagSet("shelly-proxy", flag.ContinueOnError)
// config flags
shellyHost = flagset.String("shellyplug-host", "192.168.33.1", "Shellyplug address (also via SHELLYPLUG_HOST)")
shellyPort = flagset.Int("shellyplug-port", 80, "Shellyplug port (also via SHELLYPLUG_HOST)")
//shellyAuthUser = flagset.String("shellyplug-auth-username", "", "Shellyplug authentication username (also via SHELLYPLUG_AUTH_USERNAME)")
//shellyAuthPass = flagset.String("shellyplug-auth-password", "", "Shellyplug authentication username (also via SHELLYPLUG_AUTH_PASSWORD)")
exporterPort = flagset.Int("exporter-port", 5000, "Exporter port (also via EXPORTER_PORT)")
exporterName = flagset.String("exporter-name", "shelly_plug", "Exporter name (also via EXPORTER_NAME)")
// additional flags
_ = flagset.String("config", "", "Path to config file (ini format)")
//versionInfo = flagset.Bool("version", false, "Show version information")
// internal
ss *ShellyStatus
)
type ShellyStatus struct {
Status1 *ShellyStatus1
Status2 *ShellyStatus2
Version int
}
type ShellyVersion struct {
Gen int `json:"gen"`
Type string `json:"type"`
}
func main() {
// use .env file if it exists
if _, err := os.Stat(".env"); err == nil {
if err := ff.Parse(flagset, os.Args[1:],
ff.WithEnvVarPrefix(""),
ff.WithConfigFile(".env"),
ff.WithConfigFileParser(ff.EnvParser),
); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
} else {
// use env variables and shelly-proxy.ini file
if err := ff.Parse(flagset, os.Args[1:],
ff.WithEnvVarPrefix(""),
ff.WithConfigFileFlag("config"),
ff.WithConfigFileParser(IniParser),
); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
ss = &ShellyStatus{}
ss.Fetch()
c := cron.New(cron.WithLocation(time.UTC))
if _, err := c.AddFunc("@every 1m", func() { ss.Fetch() }); err != nil {
log.Fatalf("cron.AddFunc (%s)", err)
}
c.Start()
gin.SetMode(gin.ReleaseMode)
r := gin.Default()
r.GET("/metrics", GetMetrics)
if err := r.Run(fmt.Sprintf(":%d", *exporterPort)); err != nil {
log.Fatalf("router.Run (%s)", err)
}
}
func (s *ShellyStatus) GetVersion() int {
if s.Version != 0 {
return s.Version
}
resp, err := http.Get(fmt.Sprintf("http://%s:%d/shelly", *shellyHost, *shellyPort))
if err != nil {
log.Fatalf("ShellyStatus.GetVersion : http.Get (%s)", err)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatalf("ShellyStatus.GetVersion : io.ReadAll (%s)", err)
}
v := ShellyVersion{}
if err = json.Unmarshal(body, &v); err != nil {
log.Fatalf("ShellyStatus.GetVersion : json.Unmarshal (%s)", err)
}
if v.Type != "" {
s.Version = 1
s.Status1 = &ShellyStatus1{}
}
if v.Gen != 0 {
s.Version = v.Gen
s.Status2 = &ShellyStatus2{}
}
return s.Version
}
func (s *ShellyStatus) Fetch() {
switch s.GetVersion() {
case 1:
resp, err := http.Get(fmt.Sprintf("http://%s:%d/status", *shellyHost, *shellyPort))
if err != nil {
log.Fatalf("ShellyStatus.Fetch : http.Get (%s)", err)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatalf("ShellyStatus.Fetch : io.ReadAll (%s)", err)
}
if err = json.Unmarshal(body, s.Status1); err != nil {
log.Fatalf("ShellyStatus.Fetch : json.Unmarshal (%s)", err)
}
return
case 2:
resp, err := http.Get(fmt.Sprintf("http://%s:%d/rpc/Shelly.GetStatus", *shellyHost, *shellyPort))
if err != nil {
log.Fatalf("ShellyStatus.Fetch : http.Get (%s)", err)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatalf("ShellyStatus.Fetch : io.ReadAll (%s)", err)
}
if err = json.Unmarshal(body, s.Status2); err != nil {
log.Fatalf("ShellyStatus.Fetch : json.Unmarshal (%s)", err)
}
return
default:
return
}
}
func GetMetrics(c *gin.Context) {
switch ss.Version {
case 1:
c.String(http.StatusOK, `shellyplug_name %s
# HELP shellyplug_power Current power drawn in watts
# TYPE shellyplug_power gauge
shellyplug_power %.2f
# HELP shellyplug_overpower Overpower drawn in watts/minute
# TYPE shellyplug_overpower gauge
shellyplug_overpower %.2f
# HELP shellyplug_total_power Total power consumed in watts/minute
# TYPE shellyplug_total_power counter
shellyplug_total_power %d
# HELP shellyplug_temperature Plug temperature in celsius
# TYPE shellyplug_temperature gauge
shellyplug_temperature %.2f
# HELP shellyplug_uptime Plug uptime in seconds
# TYPE shellyplug_uptime gauge
shellyplug_uptime %d
`, exporterName, ss.Status1.Meters[0].Counters[0], float32(0), 0, ss.Status1.Temperature, ss.Status1.Uptime)
case 2:
c.String(http.StatusOK, `shellyplug_name %s
# HELP shellyplug_power Current power drawn in watts
# TYPE shellyplug_power gauge
shellyplug_power %.2f
# HELP shellyplug_overpower Overpower drawn in watts/minute
# TYPE shellyplug_overpower gauge
shellyplug_overpower %.2f
# HELP shellyplug_total_power Total power consumed in watts/minute
# TYPE shellyplug_total_power counter
shellyplug_total_power %d
# HELP shellyplug_temperature Plug temperature in celsius
# TYPE shellyplug_temperature gauge
shellyplug_temperature %.2f
# HELP shellyplug_uptime Plug uptime in seconds
# TYPE shellyplug_uptime gauge
shellyplug_uptime %d
`, exporterName, ss.Status2.Switch.Power, float32(0), 0, ss.Status2.Switch.Temperature.Celsius, ss.Status2.Sys.Uptime)
default:
c.String(http.StatusOK, `shellyplug_name %s
# HELP shellyplug_power Current power drawn in watts
# TYPE shellyplug_power gauge
shellyplug_power %.2f
# HELP shellyplug_overpower Overpower drawn in watts/minute
# TYPE shellyplug_overpower gauge
shellyplug_overpower %.2f
# HELP shellyplug_total_power Total power consumed in watts/minute
# TYPE shellyplug_total_power counter
shellyplug_total_power %d
# HELP shellyplug_temperature Plug temperature in celsius
# TYPE shellyplug_temperature gauge
shellyplug_temperature %.2f
# HELP shellyplug_uptime Plug uptime in seconds
# TYPE shellyplug_uptime gauge
shellyplug_uptime %d
`, exporterName, float32(0), float32(0), 0, float32(0), 0)
}
}
// IniParser is a parser for config files in classic key/value style format. Each
// line is tokenized as a single key/value pair. The first "=" delimited
// token in the line is interpreted as the flag name, and all remaining tokens
// are interpreted as the value. Any leading hyphens on the flag name are
// ignored.
func IniParser(r io.Reader, set func(name, value string) error) error {
s := bufio.NewScanner(r)
for s.Scan() {
line := strings.TrimSpace(s.Text())
if line == "" {
continue // skip empties
}
if line[0] == '#' || line[0] == ';' {
continue // skip comments
}
var (
name string
value string
index = strings.IndexRune(line, '=')
)
if index < 0 {
name, value = line, "true" // boolean option
} else {
name, value = strings.TrimSpace(line[:index]), strings.Trim(strings.TrimSpace(line[index+1:]), "\"")
}
if i := strings.Index(value, " #"); i >= 0 {
value = strings.TrimSpace(value[:i])
}
if err := set(name, value); err != nil {
return err
}
}
return nil
}