2023-08-10 12:17:26 +02:00
|
|
|
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
|
2023-12-16 14:18:09 +01:00
|
|
|
shellyHost = flagset.String("shellyplug-host", "192.168.33.1", "Shellyplug address (also via SHELLYPLUG_HOST)")
|
2023-08-10 12:17:26 +02:00
|
|
|
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)")
|
2023-12-16 14:18:09 +01:00
|
|
|
exporterPort = flagset.Int("exporter-port", 5000, "Exporter port (also via EXPORTER_PORT)")
|
2023-12-16 14:34:23 +01:00
|
|
|
exporterName = flagset.String("exporter-name", "shelly_plug", "Exporter name (also via EXPORTER_NAME)")
|
2023-08-10 12:17:26 +02:00
|
|
|
|
|
|
|
// additional flags
|
|
|
|
_ = flagset.String("config", "", "Path to config file (ini format)")
|
|
|
|
//versionInfo = flagset.Bool("version", false, "Show version information")
|
|
|
|
|
|
|
|
// internal
|
|
|
|
ss *ShellyStatus
|
|
|
|
)
|
|
|
|
|
2023-12-16 14:18:09 +01:00
|
|
|
type ShellyStatus struct {
|
|
|
|
Status1 *ShellyStatus1
|
|
|
|
Status2 *ShellyStatus2
|
|
|
|
Version int
|
|
|
|
}
|
|
|
|
|
|
|
|
type ShellyVersion struct {
|
|
|
|
Gen int `json:"gen"`
|
|
|
|
Type string `json:"type"`
|
|
|
|
}
|
|
|
|
|
2023-08-10 12:17:26 +02:00
|
|
|
func main() {
|
|
|
|
|
|
|
|
// use .env file if it exists
|
|
|
|
if _, err := os.Stat(".env"); err == nil {
|
|
|
|
if err := ff.Parse(flagset, os.Args[1:],
|
2023-08-10 12:36:00 +02:00
|
|
|
ff.WithEnvVarPrefix(""),
|
2023-08-10 12:17:26 +02:00
|
|
|
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:],
|
2023-08-10 12:36:00 +02:00
|
|
|
ff.WithEnvVarPrefix(""),
|
2023-08-10 12:17:26 +02:00
|
|
|
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)
|
|
|
|
|
2023-08-10 12:45:44 +02:00
|
|
|
if err := r.Run(fmt.Sprintf(":%d", *exporterPort)); err != nil {
|
2023-08-10 12:44:46 +02:00
|
|
|
log.Fatalf("router.Run (%s)", err)
|
|
|
|
}
|
2023-08-10 12:17:26 +02:00
|
|
|
}
|
|
|
|
|
2023-12-16 14:18:09 +01:00
|
|
|
func (s *ShellyStatus) GetVersion() int {
|
|
|
|
if s.Version != 0 {
|
|
|
|
return s.Version
|
|
|
|
}
|
|
|
|
resp, err := http.Get(fmt.Sprintf("http://%s:%d/shelly", *shellyHost, *shellyPort))
|
2023-08-10 12:17:26 +02:00
|
|
|
if err != nil {
|
2023-12-16 14:18:09 +01:00
|
|
|
log.Fatalf("ShellyStatus.GetVersion : http.Get (%s)", err)
|
2023-08-10 12:17:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
2023-12-16 14:18:09 +01:00
|
|
|
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{}
|
2023-08-10 12:17:26 +02:00
|
|
|
}
|
|
|
|
|
2023-12-16 14:18:09 +01:00
|
|
|
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
|
2023-08-10 12:17:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func GetMetrics(c *gin.Context) {
|
2023-12-16 14:18:09 +01:00
|
|
|
switch ss.Version {
|
|
|
|
case 1:
|
2023-12-16 14:34:23 +01:00
|
|
|
c.String(http.StatusOK, `shellyplug_name %s
|
|
|
|
# HELP shellyplug_power Current power drawn in watts
|
2023-08-10 12:17:26 +02:00
|
|
|
# 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
|
2023-12-16 16:07:38 +01:00
|
|
|
`, *exporterName, ss.Status1.Meters[0].Counters[0], float32(0), 0, ss.Status1.Temperature, ss.Status1.Uptime)
|
2023-12-16 14:18:09 +01:00
|
|
|
case 2:
|
2023-12-16 14:34:23 +01:00
|
|
|
c.String(http.StatusOK, `shellyplug_name %s
|
|
|
|
# HELP shellyplug_power Current power drawn in watts
|
2023-12-16 14:18:09 +01:00
|
|
|
# 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
|
2023-12-16 16:07:38 +01:00
|
|
|
`, *exporterName, ss.Status2.Switch.Power, float32(0), 0, ss.Status2.Switch.Temperature.Celsius, ss.Status2.Sys.Uptime)
|
2023-12-16 14:18:09 +01:00
|
|
|
default:
|
2023-12-16 14:34:23 +01:00
|
|
|
c.String(http.StatusOK, `shellyplug_name %s
|
|
|
|
# HELP shellyplug_power Current power drawn in watts
|
2023-12-16 14:18:09 +01:00
|
|
|
# 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
|
2023-12-16 16:07:38 +01:00
|
|
|
`, *exporterName, float32(0), float32(0), 0, float32(0), 0)
|
2023-12-16 14:18:09 +01:00
|
|
|
}
|
|
|
|
|
2023-08-10 12:17:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|