Adds power exporter
This commit is contained in:
parent
021441b708
commit
5a25bb1c09
1
Jenkinsfile
vendored
1
Jenkinsfile
vendored
@ -176,6 +176,5 @@ sonar.go.coverage.reportPaths=cover.out
|
|||||||
junit 'report.xml'
|
junit 'report.xml'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
333
cmd/export/config.go
Normal file
333
cmd/export/config.go
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/logutils"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
// time configuration
|
||||||
|
TimeFormat string `env:"TIME_FORMAT" default:"2006-01-02 15:04:05"`
|
||||||
|
TimeZoneLocal string `env:"TIME_ZONE" default:"America/Chicago"`
|
||||||
|
TZoneLocal *time.Location `ignored:"true"`
|
||||||
|
TZoneUTC *time.Location `ignored:"true"`
|
||||||
|
|
||||||
|
// logging
|
||||||
|
LogLevel int `env:"LOG_LEVEL" default:"20"`
|
||||||
|
Log *logutils.LevelFilter `ignored:"true"`
|
||||||
|
|
||||||
|
// webserver
|
||||||
|
WebSrvPort int `env:"WEBSRV_PORT" default:"8080"`
|
||||||
|
WebSrvIP string `env:"WEBSRV_IP" default:"0.0.0.0"`
|
||||||
|
WebSrvReadTimeout int `env:"WEBSRV_TO_READ" default:"5"`
|
||||||
|
WebSrvWriteTimeout int `env:"WEBSRV_TO_WRITE" default:"2"`
|
||||||
|
WebSrvIdleTimeout int `env:"WEBSRV_TO_READ" default:"2"`
|
||||||
|
|
||||||
|
// devices
|
||||||
|
Hosts []string `ignored:"true"`
|
||||||
|
|
||||||
|
// prometheus
|
||||||
|
Prometheus configPrometheus `ignored:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type hostStruct []string
|
||||||
|
|
||||||
|
func (i *hostStruct) String() string {
|
||||||
|
return "dunno"
|
||||||
|
}
|
||||||
|
func (i *hostStruct) Set(value string) error {
|
||||||
|
*i = append(*i, value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type structInfo struct {
|
||||||
|
Name string
|
||||||
|
Alt string
|
||||||
|
Key string
|
||||||
|
Field reflect.Value
|
||||||
|
Tags reflect.StructTag
|
||||||
|
Type reflect.Type
|
||||||
|
DefaultValue interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type configPrometheus struct {
|
||||||
|
CurrentMa *prometheus.GaugeVec
|
||||||
|
VoltageMv *prometheus.GaugeVec
|
||||||
|
PowerMw *prometheus.GaugeVec
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultConfig() *config {
|
||||||
|
return &config{
|
||||||
|
Log: &logutils.LevelFilter{
|
||||||
|
Levels: []logutils.LogLevel{"TRACE", "DEBUG", "INFO", "WARNING", "ERROR"},
|
||||||
|
Writer: os.Stderr,
|
||||||
|
},
|
||||||
|
Prometheus: configPrometheus{
|
||||||
|
CurrentMa: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||||
|
Namespace: "tplink",
|
||||||
|
Name: "unknown",
|
||||||
|
Help: "unknown",
|
||||||
|
}, []string{"device"}),
|
||||||
|
VoltageMv: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||||
|
Namespace: "tplink",
|
||||||
|
Name: "volts",
|
||||||
|
Help: "input voltage",
|
||||||
|
}, []string{"device"}),
|
||||||
|
PowerMw: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||||
|
Namespace: "tplink",
|
||||||
|
Name: "watts",
|
||||||
|
Help: "current wattage",
|
||||||
|
}, []string{"device"}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *config) setLogLevel() {
|
||||||
|
switch {
|
||||||
|
case cfg.LogLevel <= 20:
|
||||||
|
cfg.Log.SetMinLevel(logutils.LogLevel("ERROR"))
|
||||||
|
case cfg.LogLevel > 20 && cfg.LogLevel <= 40:
|
||||||
|
cfg.Log.SetMinLevel(logutils.LogLevel("WARNING"))
|
||||||
|
case cfg.LogLevel > 40 && cfg.LogLevel <= 60:
|
||||||
|
cfg.Log.SetMinLevel(logutils.LogLevel("INFO"))
|
||||||
|
case cfg.LogLevel > 60 && cfg.LogLevel <= 80:
|
||||||
|
cfg.Log.SetMinLevel(logutils.LogLevel("DEBUG"))
|
||||||
|
case cfg.LogLevel > 80:
|
||||||
|
cfg.Log.SetMinLevel(logutils.LogLevel("TRACE"))
|
||||||
|
}
|
||||||
|
log.SetOutput(cfg.Log)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *config) printRunningConfig(cfgInfo []structInfo) {
|
||||||
|
log.Printf("[DEBUG] Current Running configuration Values:")
|
||||||
|
for _, info := range cfgInfo {
|
||||||
|
switch info.Type.String() {
|
||||||
|
case "string":
|
||||||
|
p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*string)
|
||||||
|
log.Printf("[DEBUG]\t%s\t\t= %s\n", info.Alt, *p)
|
||||||
|
case "bool":
|
||||||
|
p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*bool)
|
||||||
|
log.Printf("[DEBUG]\t%s\t\t= %s\n", info.Alt, strconv.FormatBool(*p))
|
||||||
|
case "int":
|
||||||
|
p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*int)
|
||||||
|
log.Printf("[DEBUG]\t%s\t\t= %s\n", info.Alt, strconv.FormatInt(int64(*p), 10))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStructInfo(spec interface{}) ([]structInfo, error) {
|
||||||
|
s := reflect.ValueOf(spec)
|
||||||
|
|
||||||
|
if s.Kind() != reflect.Pointer {
|
||||||
|
return []structInfo{}, fmt.Errorf("getstructInfo() was sent a %s instead of a pointer to a struct.\n", s.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
s = s.Elem()
|
||||||
|
if s.Kind() != reflect.Struct {
|
||||||
|
return []structInfo{}, fmt.Errorf("getstructInfo() was sent a %s instead of a struct.\n", s.Kind())
|
||||||
|
}
|
||||||
|
typeOfSpec := s.Type()
|
||||||
|
|
||||||
|
infos := make([]structInfo, 0, s.NumField())
|
||||||
|
for i := 0; i < s.NumField(); i++ {
|
||||||
|
f := s.Field(i)
|
||||||
|
ftype := typeOfSpec.Field(i)
|
||||||
|
|
||||||
|
ignored, _ := strconv.ParseBool(ftype.Tag.Get("ignored"))
|
||||||
|
if !f.CanSet() || ignored {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for f.Kind() == reflect.Pointer {
|
||||||
|
if f.IsNil() {
|
||||||
|
if f.Type().Elem().Kind() != reflect.Struct {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
f.Set(reflect.New(f.Type().Elem()))
|
||||||
|
}
|
||||||
|
f = f.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
info := structInfo{
|
||||||
|
Name: ftype.Name,
|
||||||
|
Alt: strings.ToUpper(ftype.Tag.Get("env")),
|
||||||
|
Key: ftype.Name,
|
||||||
|
Field: f,
|
||||||
|
Tags: ftype.Tag,
|
||||||
|
Type: ftype.Type,
|
||||||
|
}
|
||||||
|
if info.Alt != "" {
|
||||||
|
info.Key = info.Alt
|
||||||
|
}
|
||||||
|
info.Key = strings.ToUpper(info.Key)
|
||||||
|
if ftype.Tag.Get("default") != "" {
|
||||||
|
v, err := typeConversion(ftype.Type.String(), ftype.Tag.Get("default"))
|
||||||
|
if err != nil {
|
||||||
|
return []structInfo{}, err
|
||||||
|
}
|
||||||
|
info.DefaultValue = v
|
||||||
|
}
|
||||||
|
infos = append(infos, info)
|
||||||
|
}
|
||||||
|
return infos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func typeConversion(t, v string) (interface{}, error) {
|
||||||
|
switch t {
|
||||||
|
case "string":
|
||||||
|
return v, nil
|
||||||
|
case "int":
|
||||||
|
return strconv.ParseInt(v, 10, 0)
|
||||||
|
case "int8":
|
||||||
|
return strconv.ParseInt(v, 10, 8)
|
||||||
|
case "int16":
|
||||||
|
return strconv.ParseInt(v, 10, 16)
|
||||||
|
case "int32":
|
||||||
|
return strconv.ParseInt(v, 10, 32)
|
||||||
|
case "int64":
|
||||||
|
return strconv.ParseInt(v, 10, 64)
|
||||||
|
case "uint":
|
||||||
|
return strconv.ParseUint(v, 10, 0)
|
||||||
|
case "uint16":
|
||||||
|
return strconv.ParseUint(v, 10, 16)
|
||||||
|
case "uint32":
|
||||||
|
return strconv.ParseUint(v, 10, 32)
|
||||||
|
case "uint64":
|
||||||
|
return strconv.ParseUint(v, 10, 64)
|
||||||
|
case "float32":
|
||||||
|
return strconv.ParseFloat(v, 32)
|
||||||
|
case "float64":
|
||||||
|
return strconv.ParseFloat(v, 64)
|
||||||
|
case "complex64":
|
||||||
|
return strconv.ParseComplex(v, 64)
|
||||||
|
case "complex128":
|
||||||
|
return strconv.ParseComplex(v, 128)
|
||||||
|
case "bool":
|
||||||
|
return strconv.ParseBool(v)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("Unable to identify type.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEnvString returns string from environment variable
|
||||||
|
func getEnvString(env, def string) (val string) { //nolint:deadcode
|
||||||
|
val = os.Getenv(env)
|
||||||
|
|
||||||
|
if val == "" {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEnvInt returns int from environment variable
|
||||||
|
func getEnvInt(env string, def int) (ret int) {
|
||||||
|
val := os.Getenv(env)
|
||||||
|
|
||||||
|
if val == "" {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
ret, err := strconv.Atoi(val)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[ERROR] Environment variable is not numeric: %v\n", env)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEnvBool returns boolean from environment variable
|
||||||
|
func getEnvBool(env string, def bool) bool {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
retVal bool
|
||||||
|
val = os.Getenv(env)
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(val) == 0 {
|
||||||
|
return def
|
||||||
|
} else {
|
||||||
|
retVal, err = strconv.ParseBool(val)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[ERROR] Environment variable is not boolean: %v\n", env)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return retVal
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes the application configuration by reading default values from the struct's tags
|
||||||
|
// and environment variables. Tags processed by this process are as follows:
|
||||||
|
// `ignored:"true" env:"ENVIRONMENT_VARIABLE" default:"default value"`
|
||||||
|
func initialize() *config {
|
||||||
|
cfg := defaultConfig()
|
||||||
|
var hosts hostStruct
|
||||||
|
|
||||||
|
cfgInfo, err := getStructInfo(cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[FATAL] %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, info := range cfgInfo {
|
||||||
|
switch info.Type.String() {
|
||||||
|
case "string":
|
||||||
|
var dv string
|
||||||
|
|
||||||
|
if info.DefaultValue != nil {
|
||||||
|
dv = info.DefaultValue.(string)
|
||||||
|
}
|
||||||
|
p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*string)
|
||||||
|
flag.StringVar(p, info.Name, getEnvString(info.Name, dv), "("+info.Key+")")
|
||||||
|
case "bool":
|
||||||
|
var dv bool
|
||||||
|
|
||||||
|
if info.DefaultValue != nil {
|
||||||
|
dv = info.DefaultValue.(bool)
|
||||||
|
}
|
||||||
|
p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*bool)
|
||||||
|
flag.BoolVar(p, info.Name, getEnvBool(info.Name, dv), "("+info.Key+")")
|
||||||
|
case "int":
|
||||||
|
var dv int
|
||||||
|
|
||||||
|
if info.DefaultValue != nil {
|
||||||
|
dv = int(info.DefaultValue.(int64))
|
||||||
|
}
|
||||||
|
p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*int)
|
||||||
|
flag.IntVar(p, info.Name, getEnvInt(info.Name, dv), "("+info.Key+")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flag.Var(&hosts, "host", "Host to monitor")
|
||||||
|
flag.Parse()
|
||||||
|
cfg.Hosts = hosts
|
||||||
|
|
||||||
|
// set logging level
|
||||||
|
cfg.setLogLevel()
|
||||||
|
|
||||||
|
// timezone & format configuration
|
||||||
|
cfg.TZoneUTC, _ = time.LoadLocation("UTC")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[ERROR] Unable to parse timezone string. Please use one of the timezone database values listed here: %s", "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones")
|
||||||
|
}
|
||||||
|
cfg.TZoneLocal, err = time.LoadLocation(cfg.TimeZoneLocal)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[ERROR] Unable to parse timezone string. Please use one of the timezone database values listed here: %s", "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones")
|
||||||
|
}
|
||||||
|
time.Now().Format(cfg.TimeFormat)
|
||||||
|
|
||||||
|
prometheus.MustRegister(cfg.Prometheus.CurrentMa, cfg.Prometheus.VoltageMv, cfg.Prometheus.PowerMw)
|
||||||
|
|
||||||
|
// print running config
|
||||||
|
cfg.printRunningConfig(cfgInfo)
|
||||||
|
|
||||||
|
log.Println("[INFO] initialization complete")
|
||||||
|
return cfg
|
||||||
|
}
|
72
cmd/export/httpServer.go
Normal file
72
cmd/export/httpServer.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func httpAccessLog(req *http.Request) {
|
||||||
|
log.Printf("[TRACE] %s - %s - %s\n", req.Method, req.RemoteAddr, req.RequestURI)
|
||||||
|
}
|
||||||
|
|
||||||
|
func crossSiteOrigin(w http.ResponseWriter) {
|
||||||
|
w.Header().Add("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *config) httpServer() {
|
||||||
|
path := http.NewServeMux()
|
||||||
|
|
||||||
|
connection := &http.Server{
|
||||||
|
Addr: fmt.Sprintf("%s:%d", config.WebSrvIP, config.WebSrvPort),
|
||||||
|
Handler: path,
|
||||||
|
ReadTimeout: time.Duration(config.WebSrvReadTimeout) * time.Second,
|
||||||
|
WriteTimeout: time.Duration(config.WebSrvWriteTimeout) * time.Second,
|
||||||
|
IdleTimeout: time.Duration(config.WebSrvIdleTimeout) * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// metrics
|
||||||
|
path.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
httpAccessLog(r)
|
||||||
|
crossSiteOrigin(w)
|
||||||
|
promhttp.Handler().ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
// healthcheck
|
||||||
|
path.HandleFunc("/healthcheck", webHealthCheck)
|
||||||
|
// root
|
||||||
|
path.HandleFunc("/", webRoot)
|
||||||
|
|
||||||
|
if err := connection.ListenAndServe(); err != nil {
|
||||||
|
log.Fatalf("[ERROR] %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func webRoot(w http.ResponseWriter, r *http.Request) {
|
||||||
|
httpAccessLog(r)
|
||||||
|
crossSiteOrigin(w)
|
||||||
|
|
||||||
|
if strings.ToLower(r.Method) == "get" {
|
||||||
|
tmpltWebRoot(w)
|
||||||
|
} else {
|
||||||
|
log.Printf("[DEBUG] Request to '/' was made using the wrong method: expected %s, got %s\n", "GET", strings.ToUpper(r.Method))
|
||||||
|
tmpltError(w, http.StatusBadRequest, "Invalid http method.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func webHealthCheck(w http.ResponseWriter, r *http.Request) {
|
||||||
|
httpAccessLog(r)
|
||||||
|
crossSiteOrigin(w)
|
||||||
|
|
||||||
|
if strings.ToLower(r.Method) == "get" {
|
||||||
|
tmpltHealthCheck(w)
|
||||||
|
} else {
|
||||||
|
log.Printf("[DEBUG] Request to '/healthcheck' was made using the wrong method: expected %s, got %s\n", "GET", strings.ToUpper(r.Method))
|
||||||
|
tmpltError(w, http.StatusBadRequest, "Invalid http method.")
|
||||||
|
}
|
||||||
|
}
|
68
cmd/export/httpTemplates.go
Normal file
68
cmd/export/httpTemplates.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func tmpltError(w http.ResponseWriter, s int, m string) {
|
||||||
|
var (
|
||||||
|
output []byte
|
||||||
|
o = struct {
|
||||||
|
Error bool `json:"error" yaml:"error"`
|
||||||
|
ErrorMsg string `json:"errorMessage" yaml:"errorMessage"`
|
||||||
|
}{
|
||||||
|
Error: true,
|
||||||
|
ErrorMsg: m,
|
||||||
|
}
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
output, err = json.MarshalIndent(o, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[TRACE] Unable to marshal error message: %v.", err)
|
||||||
|
}
|
||||||
|
w.WriteHeader(s)
|
||||||
|
w.Write(output) //nolint:errcheck
|
||||||
|
}
|
||||||
|
|
||||||
|
func tmpltWebRoot(w http.ResponseWriter) {
|
||||||
|
o := struct {
|
||||||
|
Application string `json:"application" yaml:"application"`
|
||||||
|
Description string `json:"description" yaml:"description"`
|
||||||
|
Version string `json:"version" yaml:"version"`
|
||||||
|
}{
|
||||||
|
Application: "TP-Link Web API",
|
||||||
|
Description: "API for automated calls to TP-Link devices",
|
||||||
|
Version: "v1.0.0",
|
||||||
|
}
|
||||||
|
w.Header().Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
output, err := json.MarshalIndent(o, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[TRACE] Unable to marshal error message: %v.", err)
|
||||||
|
}
|
||||||
|
w.Write(output) //nolint:errcheck
|
||||||
|
}
|
||||||
|
|
||||||
|
func tmpltHealthCheck(w http.ResponseWriter) {
|
||||||
|
o := struct {
|
||||||
|
WebServer bool `json:"webServerActive" yaml:"webServerActive"`
|
||||||
|
Status string `json:"status" yaml:"status"`
|
||||||
|
}{
|
||||||
|
WebServer: true,
|
||||||
|
Status: "healthy",
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := json.MarshalIndent(o, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[TRACE] Unable to marshal status message: %v.", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "application/json")
|
||||||
|
w.Write(output) //nolint:errcheck
|
||||||
|
}
|
31
cmd/export/main.go
Normal file
31
cmd/export/main.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"os/signal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func forever() {
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
|
sig := <-c
|
||||||
|
log.Printf("[INFO] shutting down, detected signal: %s", sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := initialize()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
log.Println("[DEBUG] shutdown sequence complete")
|
||||||
|
}()
|
||||||
|
|
||||||
|
go cfg.httpServer()
|
||||||
|
|
||||||
|
go cfg.gatherMetrics()
|
||||||
|
|
||||||
|
forever()
|
||||||
|
}
|
29
cmd/export/readMetrics.go
Normal file
29
cmd/export/readMetrics.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"tplink/internal/tplink"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (cfg *config) gatherMetrics() {
|
||||||
|
for {
|
||||||
|
for _, host := range cfg.Hosts {
|
||||||
|
d := tplink.Tplink{
|
||||||
|
Host: host,
|
||||||
|
}
|
||||||
|
m, err := d.GetMeterInto()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[WARNING] Unable to read from device: %s: %v", host, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cfg.Prometheus.CurrentMa.With(prometheus.Labels{"device": host}).Set(float64(m.Emeter.GetRealtime.CurrentMa))
|
||||||
|
cfg.Prometheus.VoltageMv.With(prometheus.Labels{"device": host}).Set(float64(m.Emeter.GetRealtime.VoltageMv))
|
||||||
|
cfg.Prometheus.PowerMw.With(prometheus.Labels{"device": host}).Set(float64(m.Emeter.GetRealtime.PowerMw))
|
||||||
|
}
|
||||||
|
time.Sleep(time.Duration(time.Second * 15))
|
||||||
|
}
|
||||||
|
}
|
8
go.mod
8
go.mod
@ -5,17 +5,17 @@ go 1.19
|
|||||||
require (
|
require (
|
||||||
github.com/hashicorp/logutils v1.0.0
|
github.com/hashicorp/logutils v1.0.0
|
||||||
github.com/kelvins/sunrisesunset v0.0.0-20210220141756-39fa1bd816d5
|
github.com/kelvins/sunrisesunset v0.0.0-20210220141756-39fa1bd816d5
|
||||||
github.com/prometheus/client_golang v1.13.0
|
github.com/prometheus/client_golang v1.14.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||||
github.com/prometheus/client_model v0.2.0 // indirect
|
github.com/prometheus/client_model v0.3.0 // indirect
|
||||||
github.com/prometheus/common v0.37.0 // indirect
|
github.com/prometheus/common v0.37.0 // indirect
|
||||||
github.com/prometheus/procfs v0.8.0 // indirect
|
github.com/prometheus/procfs v0.8.0 // indirect
|
||||||
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 // indirect
|
golang.org/x/sys v0.2.0 // indirect
|
||||||
google.golang.org/protobuf v1.28.1 // indirect
|
google.golang.org/protobuf v1.28.1 // indirect
|
||||||
)
|
)
|
||||||
|
15
go.sum
15
go.sum
@ -52,6 +52,7 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
|
|||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
@ -146,8 +147,9 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
|
|||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
@ -164,13 +166,14 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn
|
|||||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||||
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||||
github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU=
|
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
|
||||||
github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ=
|
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
|
||||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
|
||||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||||
|
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||||
@ -322,8 +325,8 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 h1:Sx/u41w+OwrInGdEckYmEuU5gHoGSL4QbDz3S9s6j4U=
|
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||||
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
@ -66,6 +66,29 @@ type SysInfo struct {
|
|||||||
Longitude float64 `json:"longitude"`
|
Longitude float64 `json:"longitude"`
|
||||||
} `json:"get_sysinfo"`
|
} `json:"get_sysinfo"`
|
||||||
} `json:"system"`
|
} `json:"system"`
|
||||||
|
Emeter struct {
|
||||||
|
GetRealtime struct {
|
||||||
|
CurrentMa int `json:"current_ma"`
|
||||||
|
VoltageMv int `json:"voltage_mv"`
|
||||||
|
PowerMw int `json:"power_mw"`
|
||||||
|
TotalWh int `json:"total_wh"`
|
||||||
|
ErrCode int `json:"err_code"`
|
||||||
|
} `json:"get_realtime"`
|
||||||
|
GetVgainIgain struct {
|
||||||
|
Vgain int `json:"vgain"`
|
||||||
|
Igain int `json:"igain"`
|
||||||
|
ErrCode int `json:"err_code"`
|
||||||
|
} `json:"get_vgain_igain"`
|
||||||
|
GetDaystat struct {
|
||||||
|
DayList []struct {
|
||||||
|
Year int `json:"year"`
|
||||||
|
Month int `json:"month"`
|
||||||
|
Day int `json:"day"`
|
||||||
|
EnergyWh int `json:"energy_wh"`
|
||||||
|
} `json:"day_list"`
|
||||||
|
ErrCode int `json:"err_code"`
|
||||||
|
} `json:"get_daystat"`
|
||||||
|
} `json:"emeter"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tplink Device host identification
|
// Tplink Device host identification
|
||||||
@ -100,6 +123,25 @@ type changeStateMultiSwitch struct {
|
|||||||
} `json:"system"`
|
} `json:"system"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type meterInfo struct {
|
||||||
|
System struct {
|
||||||
|
GetSysinfo struct{} `json:"get_sysinfo"`
|
||||||
|
} `json:"system"`
|
||||||
|
Emeter struct {
|
||||||
|
GetRealtime struct{} `json:"get_realtime"`
|
||||||
|
GetVgainIgain struct{} `json:"get_vgain_igain"`
|
||||||
|
} `json:"emeter"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dailyStats struct {
|
||||||
|
Emeter struct {
|
||||||
|
GetDaystat struct {
|
||||||
|
Month int `json:"month"`
|
||||||
|
Year int `json:"year"`
|
||||||
|
} `json:"get_daystat"`
|
||||||
|
} `json:"emeter"`
|
||||||
|
}
|
||||||
|
|
||||||
func encrypt(plaintext string) []byte {
|
func encrypt(plaintext string) []byte {
|
||||||
n := len(plaintext)
|
n := len(plaintext)
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
@ -251,3 +293,48 @@ func (s *Tplink) ChangeStateMultiSwitch(state bool) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetMeterInfo gets the power stats from a device
|
||||||
|
func (s *Tplink) GetMeterInto() (SysInfo, error) {
|
||||||
|
var (
|
||||||
|
payload meterInfo
|
||||||
|
jsonResp SysInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
j, _ := json.Marshal(payload)
|
||||||
|
|
||||||
|
data := encrypt(string(j))
|
||||||
|
resp, err := send(s.Host, data)
|
||||||
|
if err != nil {
|
||||||
|
return jsonResp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(decrypt(resp)), &jsonResp); err != nil {
|
||||||
|
return jsonResp, err
|
||||||
|
}
|
||||||
|
return jsonResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMeterInfo gets the power stats from a device
|
||||||
|
func (s *Tplink) GetDailyStats(month, year int) (SysInfo, error) {
|
||||||
|
var (
|
||||||
|
payload dailyStats
|
||||||
|
jsonResp SysInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
payload.Emeter.GetDaystat.Month = month
|
||||||
|
payload.Emeter.GetDaystat.Year = year
|
||||||
|
|
||||||
|
j, _ := json.Marshal(payload)
|
||||||
|
|
||||||
|
data := encrypt(string(j))
|
||||||
|
resp, err := send(s.Host, data)
|
||||||
|
if err != nil {
|
||||||
|
return jsonResp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(decrypt(resp)), &jsonResp); err != nil {
|
||||||
|
return jsonResp, err
|
||||||
|
}
|
||||||
|
return jsonResp, nil
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user