diff --git a/Jenkinsfile b/Jenkinsfile index 8abeed1..f813a77 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -176,6 +176,5 @@ sonar.go.coverage.reportPaths=cover.out junit 'report.xml' } } - } } \ No newline at end of file diff --git a/cmd/export/config.go b/cmd/export/config.go new file mode 100644 index 0000000..4b161c7 --- /dev/null +++ b/cmd/export/config.go @@ -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 +} diff --git a/cmd/export/httpServer.go b/cmd/export/httpServer.go new file mode 100644 index 0000000..9ead82b --- /dev/null +++ b/cmd/export/httpServer.go @@ -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.") + } +} diff --git a/cmd/export/httpTemplates.go b/cmd/export/httpTemplates.go new file mode 100644 index 0000000..a3f4666 --- /dev/null +++ b/cmd/export/httpTemplates.go @@ -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 +} diff --git a/cmd/export/main.go b/cmd/export/main.go new file mode 100644 index 0000000..f09ee98 --- /dev/null +++ b/cmd/export/main.go @@ -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() +} diff --git a/cmd/export/readMetrics.go b/cmd/export/readMetrics.go new file mode 100644 index 0000000..3dab45d --- /dev/null +++ b/cmd/export/readMetrics.go @@ -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)) + } +} diff --git a/go.mod b/go.mod index ae11bbd..57786c5 100644 --- a/go.mod +++ b/go.mod @@ -5,17 +5,17 @@ go 1.19 require ( github.com/hashicorp/logutils v1.0.0 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 ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/golang/protobuf v1.5.2 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect - github.com/prometheus/client_model v0.2.0 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.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 ) diff --git a/go.sum b/go.sum index 31abf31..1379536 100644 --- a/go.sum +++ b/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/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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.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/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/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.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-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 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.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.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU= -github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +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-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.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.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.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 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-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-20220818161305-2296e01440c6 h1:Sx/u41w+OwrInGdEckYmEuU5gHoGSL4QbDz3S9s6j4U= -golang.org/x/sys v0.0.0-20220818161305-2296e01440c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= +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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/tplink/tplink.go b/internal/tplink/tplink.go index 0b811d0..7aa94e9 100644 --- a/internal/tplink/tplink.go +++ b/internal/tplink/tplink.go @@ -66,6 +66,29 @@ type SysInfo struct { Longitude float64 `json:"longitude"` } `json:"get_sysinfo"` } `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 @@ -100,6 +123,25 @@ type changeStateMultiSwitch struct { } `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 { n := len(plaintext) buf := new(bytes.Buffer) @@ -251,3 +293,48 @@ func (s *Tplink) ChangeStateMultiSwitch(state bool) error { } 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 +}