From f6edb3444b0def769ce5d307dcf36b2da4c1ddaf Mon Sep 17 00:00:00 2001 From: Wangchong Zhou Date: Thu, 3 Oct 2019 23:37:48 -0700 Subject: [PATCH] init --- .gitignore | 1 + .goreleaser.yml | 52 +++++++++++++ README.md | 27 +++++++ exporter/exporter.go | 118 +++++++++++++++++++++++++++++ exporter/server.go | 43 +++++++++++ go.mod | 10 +++ go.sum | 69 +++++++++++++++++ kasa/emeter.go | 22 ++++++ kasa/kasa.go | 177 +++++++++++++++++++++++++++++++++++++++++++ kasa/system.go | 32 ++++++++ main.go | 14 ++++ 11 files changed, 565 insertions(+) create mode 100644 .gitignore create mode 100644 .goreleaser.yml create mode 100644 README.md create mode 100644 exporter/exporter.go create mode 100644 exporter/server.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 kasa/emeter.go create mode 100644 kasa/kasa.go create mode 100644 kasa/system.go create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e501ec --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +tplink-plug-exporter diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..b19f9be --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,52 @@ +# This is an example goreleaser.yaml file with some sane defaults. +# Make sure to check the documentation at http://goreleaser.com +before: + hooks: + # you may remove this if you don't use vgo + - go mod tidy + # you may remove this if you don't need go generate + - go generate ./... +builds: +- env: + - CGO_ENABLED=0 + goos: + - darwin + - linux + - windows + goarch: + - 386 + - amd64 + - arm + - arm64 + goarm: + - 6 + - 7 + ignore: + - goos: darwin + goarch: 386 + - goos: darwin + goarch: arm + - goos: darwin + goarch: arm64 + - goos: windows + goarch: arm + - goos: windows + goarch: arm + - goos: windows + goarch: arm64 + - goarch: arm64 + goarm: 6 +archives: +- replacements: + 386: i386 + amd64: x86_64 +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ .Tag }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' diff --git a/README.md b/README.md new file mode 100644 index 0000000..da3887e --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# tplink-plug-exporter + +## Sample prometheus config + +```yaml +# scrape kasa devices +scrape_configs: + - job_name: 'kasa' + static_configs: + - targets: + - 192.168.0.233 + - 192.168.0.234 + metrics_path: /scrape + relabel_configs: + - source_labels : [__address__] + target_label: __param_target + - source_labels: [__param_target] + target_label: instance + - target_label: __address__ + replacement: localhost:9233 + +# scrape kasa_exporter itself + - job_name: 'kasa_exporter' + static_configs: + - targets: + - localhost:9233 +``` \ No newline at end of file diff --git a/exporter/exporter.go b/exporter/exporter.go new file mode 100644 index 0000000..2a99491 --- /dev/null +++ b/exporter/exporter.go @@ -0,0 +1,118 @@ +package exporter + +import ( + "github.com/fffonion/tplink-plug-exporter/kasa" + "github.com/prometheus/client_golang/prometheus" +) + +type Exporter struct { + target string + client *kasa.KasaClient + + metricsUp, + metricsRelayState, + metricsOnTime, + metricsRssi, + metricsCurrent, + metricsVoltage, + metricsPowerLoad, + metricsPowerTotal *prometheus.Desc +} + +type ExporterTarget struct { + Host string +} + +func NewExporter(t *ExporterTarget) *Exporter { + var ( + constLabels = prometheus.Labels{} + labelNames = []string{"alias"} + ) + + e := &Exporter{ + target: t.Host, + client: kasa.New(&kasa.KasaClientConfig{ + Host: t.Host, + }), + metricsUp: prometheus.NewDesc("kasa_online", + "Device online.", + nil, constLabels, + ), + metricsRelayState: prometheus.NewDesc("kasa_relay_state", + "Relay state (switch on/off).", + labelNames, constLabels, + ), + metricsOnTime: prometheus.NewDesc("kasa_on_time", + "Time in seconds since online.", + labelNames, constLabels), + metricsRssi: prometheus.NewDesc("kasa_rssi", + "Wifi received signal strength indicator.", + labelNames, constLabels), + metricsCurrent: prometheus.NewDesc("kasa_current", + "Current flowing through device in Ampere.", + labelNames, constLabels), + metricsVoltage: prometheus.NewDesc("kasa_voltage", + "Current voltage connected to device in Volt.", + labelNames, constLabels), + metricsPowerLoad: prometheus.NewDesc("kasa_power_load", + "Power of load in Watt.", + labelNames, constLabels), + metricsPowerTotal: prometheus.NewDesc("kasa_power_total", + "Power of load and device itself in Watt.", + labelNames, constLabels), + } + return e +} + +func (k *Exporter) Describe(ch chan<- *prometheus.Desc) { + ch <- k.metricsUp + ch <- k.metricsRelayState + ch <- k.metricsOnTime + ch <- k.metricsRssi + ch <- k.metricsCurrent + ch <- k.metricsVoltage + ch <- k.metricsPowerLoad + ch <- k.metricsPowerTotal + +} + +func (k *Exporter) Collect(ch chan<- prometheus.Metric) { + s := k.client.SystemService() + r, err := s.GetSysInfo() + + if err != nil { + ch <- prometheus.MustNewConstMetric(k.metricsUp, prometheus.GaugeValue, + 0) + return + } + alias := r.Alias + + ch <- prometheus.MustNewConstMetric(k.metricsRelayState, prometheus.GaugeValue, + float64(r.RelayState), alias) + ch <- prometheus.MustNewConstMetric(k.metricsOnTime, prometheus.CounterValue, + float64(r.OnTime), alias) + ch <- prometheus.MustNewConstMetric(k.metricsRssi, prometheus.GaugeValue, + float64(r.RSSI), alias) + + if s.EmeterSupported(r) { + m := k.client.EmeterService() + r, err := m.GetRealtime() + + if err != nil { + ch <- prometheus.MustNewConstMetric(k.metricsUp, prometheus.GaugeValue, + 0) + return + } + ch <- prometheus.MustNewConstMetric(k.metricsCurrent, prometheus.GaugeValue, + float64(r.Current), alias) + ch <- prometheus.MustNewConstMetric(k.metricsVoltage, prometheus.GaugeValue, + float64(r.Voltage), alias) + ch <- prometheus.MustNewConstMetric(k.metricsPowerLoad, prometheus.GaugeValue, + float64(r.Power), alias) + ch <- prometheus.MustNewConstMetric(k.metricsPowerTotal, prometheus.GaugeValue, + float64(r.Total), alias) + } + + ch <- prometheus.MustNewConstMetric(k.metricsUp, prometheus.GaugeValue, + 1) +} diff --git a/exporter/server.go b/exporter/server.go new file mode 100644 index 0000000..7ee50d8 --- /dev/null +++ b/exporter/server.go @@ -0,0 +1,43 @@ +package exporter + +import ( + "net/http" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +type HttpServer struct { + mux *http.ServeMux +} + +func NewHttpServer() *HttpServer { + s := &HttpServer{ + mux: http.NewServeMux(), + } + + s.mux.HandleFunc("/scrape", s.ScrapeHandler) + return s +} + +func (s *HttpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + s.mux.ServeHTTP(w, r) +} + +//https://github.com/oliver006/redis_exporter/blob/master/exporter.go +func (s *HttpServer) ScrapeHandler(w http.ResponseWriter, r *http.Request) { + target := r.URL.Query().Get("target") + if target == "" { + http.Error(w, "'target' parameter must be specified", 400) + //e.targetScrapeRequestErrors.Inc() + return + } + + registry := prometheus.NewRegistry() + e := NewExporter(&ExporterTarget{ + Host: target, + }) + registry.MustRegister(e) + + promhttp.HandlerFor(registry, promhttp.HandlerOpts{}).ServeHTTP(w, r) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9bb9079 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/fffonion/tplink-plug-exporter + +go 1.12 + +require ( + github.com/mitchellh/mapstructure v1.1.2 + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/prometheus/client_golang v1.1.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ac65a2c --- /dev/null +++ b/go.sum @@ -0,0 +1,69 @@ +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +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/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +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= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/kasa/emeter.go b/kasa/emeter.go new file mode 100644 index 0000000..9c44eb3 --- /dev/null +++ b/kasa/emeter.go @@ -0,0 +1,22 @@ +package kasa + +type KasaClientEmeterService struct { + c *KasaClient +} + +type GetRealtimeRequest struct { +} + +type GetRealtimeResponse struct { + Current float64 `mapstructure:"current"` + Voltage float64 `mapstructure:"voltage"` + Power float64 `mapstructure:"power"` + Total float64 `mapstructure:"total"` +} + +func (s *KasaClientEmeterService) GetRealtime() (*GetRealtimeResponse, error) { + var response GetRealtimeResponse + err := s.c.RPC("emeter", "get_realtime", GetRealtimeRequest{}, &response) + + return &response, err +} diff --git a/kasa/kasa.go b/kasa/kasa.go new file mode 100644 index 0000000..aa4d796 --- /dev/null +++ b/kasa/kasa.go @@ -0,0 +1,177 @@ +package kasa + +import ( + "encoding/json" + "fmt" + "io" + "net" + "time" + + "github.com/mitchellh/mapstructure" +) + +type KasaClient struct { + addr string + model string + conn *net.Conn +} + +type KasaClientConfig struct { + Host string +} + +type RPCResponse struct { + ErrCode int `json:"err_code"` +} + +func New(c *KasaClientConfig) *KasaClient { + return &KasaClient{ + addr: c.Host + ":9999", + } +} + +const key = 171 + +func packInt(in int32) []byte { + out := make([]byte, 4) + for i := 3; i > 0; i-- { + out[i] = byte(in & 0xFF) + in >>= 8 + } + return out +} + +func unpackInt(in []byte) int32 { + length := 0 + for i := 0; i < 4; i++ { + length <<= 8 + length += int(in[i]) + } + return int32(length) +} + +func encrypt(in []byte) []byte { + length := len(in) + out := make([]byte, length) + + key := key + for i, r := range in { + key = key ^ int(r) + out[i] = byte(key) + } + return out +} + +func decrypt(in []byte) []byte { + length := len(in) + out := make([]byte, length) + key := key + for i := 0; i < length; i++ { + b := int(in[i]) + out[i] = byte(key ^ b) + key = b + } + return out +} + +func (c *KasaClient) Request(payload interface{}) ([]byte, error) { + conn, err := net.Dial("tcp", c.addr) + if err != nil { + return nil, err + } + // the tcp server on smart plug can only handle one connection + // at a time, make sure we don't wait too long to block others + conn.SetWriteDeadline(time.Now().Add(time.Second)) + + jpayload, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("error marshaling payload: %v", err) + } + + _, err = conn.Write(packInt(int32(len(jpayload)))) + if err != nil { + return nil, err + } + _, err = conn.Write(encrypt(jpayload)) + if err != nil { + return nil, err + } + + conn.SetReadDeadline(time.Now().Add(time.Second)) + + var buf []byte + tmp := make([]byte, 1024) + + read := -1 + length := 0 + for read < length { + n, err := conn.Read(tmp) + if err != nil { + if err != io.EOF { + return nil, err + } + break + } + read += n + if length == 0 { + length = int(unpackInt(tmp[:4])) + tmp = tmp[4:] + n -= 4 + } + buf = append(buf, tmp[:n]...) + + } + err = conn.Close() + if err != nil { + return nil, err + } + buf = decrypt(buf) + + return buf, nil +} + +func (c *KasaClient) RPC(service string, cmd string, payload interface{}, out interface{}) error { + payload = map[string]interface{}{ + service: map[string]interface{}{ + cmd: payload, + }, + } + + response, err := c.Request(payload) + if err != nil { + return err + } + + var outMarshal map[string]map[string]map[string]interface{} + + err = json.Unmarshal(response, &outMarshal) + if err != nil { + return err + } + + if outMarshal[service] == nil || outMarshal[service][cmd] == nil { + return fmt.Errorf("malformed response: %v", outMarshal) + } + var r RPCResponse + mapstructure.Decode(outMarshal[service][cmd], &r) + if r.ErrCode != 0 { + return fmt.Errorf("rpc error: %v", outMarshal) + } + + mapstructure.Decode(outMarshal[service][cmd], &out) + + return nil + +} + +func (c *KasaClient) SystemService() *KasaClientSystemService { + return &KasaClientSystemService{ + c: c, + } +} + +func (c *KasaClient) EmeterService() *KasaClientEmeterService { + return &KasaClientEmeterService{ + c: c, + } +} diff --git a/kasa/system.go b/kasa/system.go new file mode 100644 index 0000000..21144d0 --- /dev/null +++ b/kasa/system.go @@ -0,0 +1,32 @@ +package kasa + +import "strings" + +type KasaClientSystemService struct { + c *KasaClient +} + +type GetSysInfoRequest struct { +} + +type GetSysInfoResponse struct { + MAC string `mapstructure:"mac"` + Model string `mapstructure:"model"` + Alias string `mapstructure:"alias"` + Feature string `mapstructure:"feature"` + RelayState int `mapstructure:"relay_state"` + RSSI int `mapstructure:"rssi"` + LEDOff int `mapstructure:"led_off"` + OnTime int `mapstructure:"on_time"` +} + +func (s *KasaClientSystemService) GetSysInfo() (*GetSysInfoResponse, error) { + var response GetSysInfoResponse + err := s.c.RPC("system", "get_sysinfo", GetSysInfoRequest{}, &response) + + return &response, err +} + +func (s *KasaClientSystemService) EmeterSupported(r *GetSysInfoResponse) bool { + return strings.Contains(r.Feature, "ENE") +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..949ab26 --- /dev/null +++ b/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "net/http" + + "github.com/fffonion/tplink-plug-exporter/exporter" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +func main() { + s := exporter.NewHttpServer() + http.Handle("/metrics", promhttp.Handler()) + http.ListenAndServe(":9233", s) +}