prometheus-tplink-plug-exporter-0.2.0+git20200622.cc4a731/.gitignore0000644000000000000000000000002513674126155022644 0ustar0000000000000000tplink-plug-exporter prometheus-tplink-plug-exporter-0.2.0+git20200622.cc4a731/.goreleaser.yml0000644000000000000000000000161013674126155023606 0ustar0000000000000000# 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:' prometheus-tplink-plug-exporter-0.2.0+git20200622.cc4a731/Dockerfile0000644000000000000000000000034413674126155022652 0ustar0000000000000000FROM golang:1.12-alpine AS builder ADD . /src RUN apk add --no-cache git WORKDIR /src RUN go build main.go FROM alpine:latest COPY --from=builder /src/main /tplink-plug-exporter EXPOSE 9233 ENTRYPOINT ["/tplink-plug-exporter"] prometheus-tplink-plug-exporter-0.2.0+git20200622.cc4a731/LICENSE0000644000000000000000000000242013674126155021662 0ustar0000000000000000Copyright (c) 2020, by fffonion fffonion@gmail.com. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. prometheus-tplink-plug-exporter-0.2.0+git20200622.cc4a731/README.md0000644000000000000000000000225613674126155022143 0ustar0000000000000000# tplink-plug-exporter Export TP-Link Smart Plug metrics to grafana dashboard ## Install Download from [releases](https://github.com/fffonion/tplink-plug-exporter/releases) or run from docker ``` docker run -d -p 9233:9233 fffonion/tplink-plug-exporter ``` ## Grafana dashboard Search for `Kasa` inside grafana or install from https://grafana.com/grafana/dashboards/10957 ![img](https://grafana.com/api/dashboards/10957/images/6954/image) ## Sample prometheus config ```yaml # scrape kasa devices scrape_configs: - job_name: 'kasa' static_configs: - targets: # IP of your smart plugs - 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__ # IP of the exporter replacement: localhost:9233 # scrape kasa_exporter itself - job_name: 'kasa_exporter' static_configs: - targets: # IP of the exporter - localhost:9233 ``` ## See also - Original reverse engineering work: https://github.com/softScheck/tplink-smartplug prometheus-tplink-plug-exporter-0.2.0+git20200622.cc4a731/exporter/0000755000000000000000000000000013674126155022527 5ustar0000000000000000prometheus-tplink-plug-exporter-0.2.0+git20200622.cc4a731/go.mod0000644000000000000000000000073613674126155021773 0ustar0000000000000000module github.com/fffonion/tplink-plug-exporter go 1.12 require ( github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect 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 github.com/prometheus/common v0.6.0 ) prometheus-tplink-plug-exporter-0.2.0+git20200622.cc4a731/go.sum0000644000000000000000000002003213674126155022007 0ustar0000000000000000github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= 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= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 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 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= prometheus-tplink-plug-exporter-0.2.0+git20200622.cc4a731/kasa/0000755000000000000000000000000013674126155021576 5ustar0000000000000000prometheus-tplink-plug-exporter-0.2.0+git20200622.cc4a731/main.go0000644000000000000000000000042213674126155022130 0ustar0000000000000000package main import ( "net/http" "github.com/prometheus/common/log" "github.com/fffonion/tplink-plug-exporter/exporter" ) func main() { s := exporter.NewHttpServer() log.Infoln("Accepting Prometheus Requests on :9233") log.Fatal(http.ListenAndServe(":9233", s)) } prometheus-tplink-plug-exporter-0.2.0+git20200622.cc4a731/exporter/exporter.go0000644000000000000000000000613713674126155024735 0ustar0000000000000000package 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", "Current power in Watt.", labelNames, constLabels), metricsPowerTotal: prometheus.NewDesc("kasa_power_total", "Power consumption since device connected in kWh.", 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.CounterValue, float64(r.Total), alias) } ch <- prometheus.MustNewConstMetric(k.metricsUp, prometheus.GaugeValue, 1) } prometheus-tplink-plug-exporter-0.2.0+git20200622.cc4a731/exporter/server.go0000644000000000000000000000205213674126155024363 0ustar0000000000000000package 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("/metrics", promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}).ServeHTTP) 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) } prometheus-tplink-plug-exporter-0.2.0+git20200622.cc4a731/kasa/emeter.go0000644000000000000000000000205313674126155023406 0ustar0000000000000000package kasa type KasaClientEmeterService struct { c *KasaClient } type GetRealtimeRequest struct { } type GetRealtimeResponse struct { // some fw/hw may use the following keys Current float64 `mapstructure:"current"` // unit is A Voltage float64 `mapstructure:"voltage"` // unit is V Power float64 `mapstructure:"power"` // unit is W Total float64 `mapstructure:"total"` // unit is kWh // some may use these CurrentmA float64 `mapstructure:"current_ma"` VoltagemV float64 `mapstructure:"voltage_mv"` PowermW float64 `mapstructure:"power_mw"` TotalWh float64 `mapstructure:"total_wh"` } func (r *GetRealtimeResponse) Normalize() { if r.TotalWh != -1 { r.Current = r.CurrentmA / 1000 r.Voltage = r.VoltagemV / 1000 r.Power = r.PowermW / 1000 r.Total = r.TotalWh / 1000 } } func (s *KasaClientEmeterService) GetRealtime() (*GetRealtimeResponse, error) { response := GetRealtimeResponse{ TotalWh: -1, } err := s.c.RPC("emeter", "get_realtime", GetRealtimeRequest{}, &response) response.Normalize() return &response, err } prometheus-tplink-plug-exporter-0.2.0+git20200622.cc4a731/kasa/kasa.go0000644000000000000000000000604213674126155023046 0ustar0000000000000000package kasa import ( "encoding/json" "fmt" "io" "net" "time" "github.com/mitchellh/mapstructure" ) type KasaClient struct { addr string model string } 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, } } prometheus-tplink-plug-exporter-0.2.0+git20200622.cc4a731/kasa/system.go0000644000000000000000000000151013674126155023446 0ustar0000000000000000package 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") }