shelly-proxy/proxy.go
2023-08-10 12:45:44 +02:00

214 lines
6.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"
)
type ShellyStatus struct {
Wifi ShellyWifiStatus `json:"wifi_sta"`
Cloud ShellyCloudStatus `json:"cloud"`
Mqtt ShellyMqttStatus `json:"mqtt"`
Time string `json:"time"`
Serial int `json:"serial"`
HasUpdate bool `json:"has_update"`
MAC string `json:"mac"`
Relays []ShellyRelaysStatus `json:"relays"`
Meters []ShellyMetersStatus `json:"meters"`
Temperature float32 `json:"temperature"`
OverTemperature bool `json:"overtemperature"`
Update ShellyUpdateStatus `json:"update"`
RamTotal int `json:"ram_total"`
RamFree int `json:"ram_free"`
FsSize int `json:"fs_size"`
FsFree int `json:"fs_free"`
Uptime int `json:"uptime"`
}
type ShellyWifiStatus struct {
Connected bool `json:"connected"`
SSID string `json:"ssid"`
IP string `json:"ip"`
RSSI int `json:"rssi"`
}
type ShellyCloudStatus struct {
Enabled bool `json:"enabled"`
Connected bool `json:"connected"`
}
type ShellyMqttStatus struct {
Connected bool `json:"connected"`
}
type ShellyRelaysStatus struct {
IsOn bool `json:"ison"`
HasTimer bool `json:"has_timer"`
Overpower bool `json:"overpower"`
}
type ShellyMetersStatus struct {
Power float32 `json:"power"`
IsValid bool `json:"is_valid"`
Timestamp int `json:"timestamp"`
Counters []float32 `json:"counters"`
Total int `json:"total"`
}
type ShellyUpdateStatus struct {
Status string `json:"status"`
HasUpdate bool `json:"has_update"`
NewVersion string `json:"new_version"`
OldVersion string `json:"old_version"`
}
var (
flagset = flag.NewFlagSet("shelly-proxy", flag.ContinueOnError)
// config flags
shellyHost = flagset.String("shellyplug-host", "192.168.30.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", 9000, "Exporter port (also via EXPORTER_PORT)")
// additional flags
_ = flagset.String("config", "", "Path to config file (ini format)")
//versionInfo = flagset.Bool("version", false, "Show version information")
// internal
ss *ShellyStatus
)
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)
}
}
log.Printf("addr : %s", *shellyHost)
log.Printf("port : %d", *shellyPort)
log.Printf("export : %d", *exporterPort)
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) Fetch() {
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); err != nil {
log.Fatalf("ShellyStatus.Fetch : json.Unmarshal (%s)", err)
}
}
func GetMetrics(c *gin.Context) {
c.String(http.StatusOK, `# 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
`, ss.Meters[0].Counters[0], float32(0), ss.Meters[0].Total, ss.Temperature, ss.Uptime)
}
// 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
}