├── .github └── workflows │ └── release.yml ├── .gitignore ├── Dockerfile ├── README.md ├── examples └── systemd │ ├── switchbot-exporter │ └── switchbot-exporter.service ├── go.mod ├── go.sum ├── main.go └── renovate.json /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: release 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: setup go 13 | uses: actions/setup-go@v5 14 | with: 15 | go-version: 1.23 16 | 17 | - name: checkout 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: release 23 | uses: goreleaser/goreleaser-action@v6 24 | with: 25 | version: latest 26 | args: release --clean 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | docker: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: checkout 34 | uses: actions/checkout@v4 35 | with: 36 | fetch-depth: 0 37 | 38 | - name: Docker meta 39 | id: meta 40 | uses: docker/metadata-action@v5 41 | with: 42 | images: nasa9084/switchbot-exporter 43 | 44 | - name: Set up Docker Buildx 45 | uses: docker/setup-buildx-action@v3 46 | 47 | - name: Login to DockerHub 48 | if: github.event_name != 'pull_request' 49 | uses: docker/login-action@v3 50 | with: 51 | username: ${{ secrets.DOCKER_USERNAME }} 52 | password: ${{ secrets.DOCKER_PASSWORD }} 53 | 54 | - name: push to docker hub 55 | uses: docker/build-push-action@v6 56 | with: 57 | platforms: linux/amd64,linux/arm64 58 | push: ${{ github.event_name != 'pull_request' }} 59 | tags: ${{ steps.meta.outputs.tags }} 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore vscode settings 2 | .vscode/ 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24 AS build 2 | COPY . . 3 | RUN GOPATH="" CGO_ENABLED=0 go build -o /switchbot_exporter 4 | 5 | FROM alpine:latest 6 | COPY --from=build /switchbot_exporter /switchbot_exporter 7 | RUN apk add --no-cache ca-certificates && update-ca-certificates 8 | ENTRYPOINT [ "/switchbot_exporter" ] 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # switchbot-exporter 2 | 3 | Exports [switchbot](https://us.switch-bot.com) device metrics for [prometheus](https://prometheus.io). 4 | 5 | ## Supported Devices / Metrics 6 | 7 | Currently supports humidity and temperature for: 8 | * Hub 2 9 | * Humidifier 10 | * Meter 11 | * Meter Plus 12 | * Indoor/Outdoor Thermo-Hygrometer 13 | 14 | Supports weight and voltage for: 15 | * Plug Mini (JP) 16 | 17 | ## Prometheus Configuration 18 | 19 | ### Static Configuration 20 | 21 | ~The switchbot exporter needs to be passed the target ID as a parameter,~ this can be done with relabelling (like [blackbox exporter](https://github.com/prometheus/blackbox_exporter)). 22 | 23 | Note: you can just query `/metrics` endpoint without target parameter to get all supported device status with release v0.5.0 24 | 25 | Change the host:port in the relabel_configs `replacement` to the host:port where the exporter is listening. 26 | 27 | #### Example Config (Static Configs): 28 | 29 | ``` yaml 30 | scrape_configs: 31 | - job_name: 'switchbot' 32 | scrape_interval: 5m # not to reach API rate limit 33 | metrics_path: /metrics 34 | static_configs: 35 | - targets: 36 | - DFA0029F2622 # Target switchbot meter 37 | relabel_configs: 38 | - source_labels: [__address__] 39 | target_label: __param_target 40 | - source_labels: [__param_target] 41 | target_label: instance 42 | - target_label: __address__ 43 | replacement: 127.0.0.1:8080 # The switchbot exporter's real ip/port 44 | ``` 45 | ### Dynamic Configuration using Service Discovery 46 | 47 | The switchbot exporter also implements http service discovery to create a prometheus target for each supported device in your account. When using service discover, the `static_configs` is not needed. Relabeling is used (see [blackbox exporter](https://github.com/prometheus/blackbox_exporter)) to convert the device's id into a url with the id as the url's target query parameter. 48 | 49 | Change the host:port in the http_sd_configs `url` and in the relabel_configs `replacement` to the host:port where the exporter is listening. 50 | 51 | #### Example Config (Dynamic Configs): 52 | 53 | ``` yaml 54 | scrape_configs: 55 | - job_name: 'switchbot' 56 | scrape_interval: 5m # not to reach API rate limit 57 | metrics_path: /metrics 58 | http_sd_configs: 59 | - url: http://127.0.0.1:8080/discover 60 | refresh_interval: 1d # no need to check for new devices very often 61 | relabel_configs: 62 | - source_labels: [__address__] 63 | target_label: __param_target 64 | - source_labels: [__param_target] 65 | target_label: instance 66 | - target_label: __address__ 67 | replacement: 127.0.0.1:8080 # The switchbot exporter's real ip/port 68 | ``` 69 | 70 | ## Limitation 71 | 72 | Only a subset of switchbot devices are currently supported. 73 | 74 | [switchbot API's request limit](https://github.com/OpenWonderLabs/SwitchBotAPI#request-limit) 75 | -------------------------------------------------------------------------------- /examples/systemd/switchbot-exporter: -------------------------------------------------------------------------------- 1 | SWITCHBOT_OPEN_TOKEN= 2 | SWITCHBOT_SECRET_KEY= 3 | WEB_LISTEN_ADDRESS= -------------------------------------------------------------------------------- /examples/systemd/switchbot-exporter.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=switchbot-exporter 3 | Documentation=https://github.com/nasa9084/switchbot-exporter#readme 4 | 5 | [Service] 6 | Restart=on-failure 7 | User=switchbot-exporter 8 | EnvironmentFile=/etc/default/switchbot-exporter 9 | ExecStart=/usr/sbin/switchbot-exporter -switchbot.open-token ${SWITCHBOT_OPEN_TOKEN} -switchbot.secret-key ${SWITCHBOT_SECRET_KEY} -web.listen-address ${WEB_LISTEN_ADDRESS} 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nasa9084/switchbot-exporter 2 | 3 | go 1.22 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | github.com/nasa9084/go-switchbot/v5 v5.1.0 9 | github.com/prometheus/client_golang v1.22.0 10 | ) 11 | 12 | require ( 13 | github.com/beorn7/perks v1.0.1 // indirect 14 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 15 | github.com/google/uuid v1.3.0 // indirect 16 | github.com/klauspost/compress v1.18.0 // indirect 17 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 18 | github.com/prometheus/client_model v0.6.1 // indirect 19 | github.com/prometheus/common v0.62.0 // indirect 20 | github.com/prometheus/procfs v0.15.1 // indirect 21 | golang.org/x/sys v0.30.0 // indirect 22 | google.golang.org/protobuf v1.36.5 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 6 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 7 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 8 | github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= 9 | github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 10 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 11 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 12 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 13 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 14 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 15 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 16 | github.com/nasa9084/go-switchbot/v5 v5.0.0 h1:GNgvYtbUVFz6KHpdCVjfpE51B+9NM369liDvlI5jnvc= 17 | github.com/nasa9084/go-switchbot/v5 v5.0.0/go.mod h1:o2hvx2sq92j8oaHrl5JYrWkxSAiXVCCEEIqmvqxqZus= 18 | github.com/nasa9084/go-switchbot/v5 v5.1.0 h1:93BQMHbLivnQhZ5bYopzfRm0wvT0tVQ/pR8P5Gbu8nw= 19 | github.com/nasa9084/go-switchbot/v5 v5.1.0/go.mod h1:o2hvx2sq92j8oaHrl5JYrWkxSAiXVCCEEIqmvqxqZus= 20 | github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= 21 | github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 22 | github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= 23 | github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= 24 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 25 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 26 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 27 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 28 | github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= 29 | github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= 30 | github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= 31 | github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 32 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 33 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 34 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 35 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 36 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 37 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 38 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 39 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 40 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 41 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 42 | google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= 43 | google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 44 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 45 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 46 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "log" 10 | "net/http" 11 | "os" 12 | "os/signal" 13 | "syscall" 14 | 15 | switchbot "github.com/nasa9084/go-switchbot/v5" 16 | "github.com/prometheus/client_golang/prometheus" 17 | "github.com/prometheus/client_golang/prometheus/promhttp" 18 | ) 19 | 20 | var ( 21 | listenAddress = flag.String("web.listen-address", ":8080", "The address to listen on for HTTP requests") 22 | openToken = flag.String("switchbot.open-token", "", "The open token for switchbot-api") 23 | secretKey = flag.String("switchbot.secret-key", "", "The secret key for switchbot-api") 24 | ) 25 | 26 | // deviceLabels is global cache gauge which stores device id and device name as its label. 27 | var deviceLabels = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 28 | Namespace: "switchbot", 29 | Name: "device", 30 | }, []string{"device_id", "device_name"}) 31 | 32 | // the type expected by the prometheus http service discovery 33 | type StaticConfig struct { 34 | Targets []string `json:"targets"` 35 | Labels map[string]string `json:"labels"` 36 | } 37 | 38 | type Handler struct { 39 | switchbotClient *switchbot.Client 40 | } 41 | 42 | func main() { 43 | flag.Parse() 44 | if err := run(); err != nil { 45 | log.Printf("error: %v", err) 46 | os.Exit(1) 47 | } 48 | } 49 | 50 | func run() error { 51 | openTokenFromEnv := os.Getenv("SWITCHBOT_OPENTOKEN") 52 | if openTokenFromEnv != "" { 53 | *openToken = openTokenFromEnv 54 | } 55 | 56 | if *openToken == "" { 57 | return errors.New("-switchbot.open-token is required") 58 | } 59 | 60 | secretKeyFromEnv := os.Getenv("SWITCHBOT_SECRETKEY") 61 | if secretKeyFromEnv != "" { 62 | *secretKey = secretKeyFromEnv 63 | } 64 | 65 | if *secretKey == "" { 66 | return errors.New("-switchbot.secret-key is required") 67 | } 68 | 69 | sc := switchbot.New(*openToken, *secretKey) 70 | 71 | if err := reloadDevices(sc); err != nil { 72 | return err 73 | } 74 | 75 | hup := make(chan os.Signal, 1) 76 | reloadCh := make(chan chan error) 77 | signal.Notify(hup, syscall.SIGHUP) 78 | 79 | go func() { 80 | // reload 81 | for { 82 | select { 83 | case <-hup: 84 | if err := reloadDevices(sc); err != nil { 85 | log.Printf("error reloading devices: %v", err) 86 | } 87 | log.Print("reloaded devices") 88 | case errCh := <-reloadCh: 89 | if err := reloadDevices(sc); err != nil { 90 | log.Printf("error relaoding devices: %v", err) 91 | errCh <- err 92 | } else { 93 | errCh <- nil 94 | } 95 | log.Print("relaoded devices") 96 | } 97 | } 98 | }() 99 | 100 | h := &Handler{switchbotClient: sc} 101 | 102 | http.HandleFunc("/discover", h.Discover) 103 | 104 | http.HandleFunc("/-/reload", func(w http.ResponseWriter, r *http.Request) { 105 | if expectMethod := http.MethodPost; r.Method != expectMethod { 106 | w.WriteHeader(http.StatusMethodNotAllowed) 107 | fmt.Fprintf(w, "This endpoint requires a %s request.\n", expectMethod) 108 | return 109 | } 110 | 111 | rc := make(chan error) 112 | reloadCh <- rc 113 | if err := <-rc; err != nil { 114 | http.Error(w, fmt.Sprintf("failed to reload config: %s", err), http.StatusInternalServerError) 115 | } 116 | }) 117 | 118 | http.HandleFunc("/metrics", h.Metrics) 119 | 120 | srv := &http.Server{Addr: *listenAddress} 121 | srvc := make(chan error) 122 | term := make(chan os.Signal, 1) 123 | signal.Notify(term, os.Interrupt, syscall.SIGTERM) 124 | 125 | go func() { 126 | log.Printf("listen on %s", *listenAddress) 127 | 128 | if err := srv.ListenAndServe(); err != http.ErrServerClosed { 129 | srvc <- err 130 | } 131 | }() 132 | 133 | for { 134 | select { 135 | case <-term: 136 | log.Print("received terminate signal") 137 | return nil 138 | case err := <-srvc: 139 | return err 140 | } 141 | } 142 | } 143 | 144 | func reloadDevices(sc *switchbot.Client) error { 145 | log.Print("reload device list") 146 | devices, infrared, err := sc.Device().List(context.Background()) 147 | if err != nil { 148 | return fmt.Errorf("getting device list: %w", err) 149 | } 150 | log.Print("got device list") 151 | 152 | for _, device := range devices { 153 | deviceLabels.WithLabelValues(device.ID, device.Name).Set(0) 154 | } 155 | for _, device := range infrared { 156 | deviceLabels.WithLabelValues(device.ID, device.Name).Set(0) 157 | } 158 | 159 | return nil 160 | } 161 | 162 | func (h *Handler) Discover(w http.ResponseWriter, r *http.Request) { 163 | log.Printf("discovering devices...") 164 | devices, _, err := h.switchbotClient.Device().List(r.Context()) 165 | if err != nil { 166 | http.Error(w, fmt.Sprintf("failed to discover devices: %s", err), http.StatusInternalServerError) 167 | return 168 | } 169 | log.Printf("discovered device count: %d", len(devices)) 170 | 171 | supportedDeviceTypes := map[switchbot.PhysicalDeviceType]struct{}{ 172 | switchbot.Hub2: {}, 173 | switchbot.Humidifier: {}, 174 | switchbot.Meter: {}, 175 | switchbot.MeterPlus: {}, 176 | switchbot.MeterPro: {}, 177 | switchbot.MeterProCO2: {}, 178 | switchbot.PlugMiniJP: {}, 179 | switchbot.WoIOSensor: {}, 180 | } 181 | 182 | data := make([]StaticConfig, len(devices)) 183 | 184 | for i, device := range devices { 185 | _, deviceTypeIsSupported := supportedDeviceTypes[device.Type] 186 | if !deviceTypeIsSupported { 187 | log.Printf("ignoring device %s with unsupported type: %s", device.ID, device.Type) 188 | continue 189 | } 190 | 191 | log.Printf("discovered device %s of type %s", device.ID, device.Type) 192 | staticConfig := StaticConfig{} 193 | staticConfig.Targets = make([]string, 1) 194 | staticConfig.Labels = make(map[string]string) 195 | 196 | staticConfig.Targets[0] = device.ID 197 | staticConfig.Labels["device_id"] = device.ID 198 | staticConfig.Labels["device_name"] = device.Name 199 | staticConfig.Labels["device_type"] = string(device.Type) 200 | 201 | data[i] = staticConfig 202 | } 203 | 204 | w.Header().Set("Content-Type", "application/json") 205 | w.WriteHeader(http.StatusOK) 206 | json.NewEncoder(w).Encode(data) 207 | } 208 | 209 | func (h *Handler) Metrics(w http.ResponseWriter, r *http.Request) { 210 | log.Print("called /metrics") 211 | 212 | registry := prometheus.NewRegistry() 213 | var targets []string 214 | 215 | if target := r.FormValue("target"); target != "" { 216 | targets = []string{target} 217 | 218 | log.Printf("target parameter is given: %s", target) 219 | } else { 220 | log.Print("target parameter is not given") 221 | devices, _, err := h.switchbotClient.Device().List(r.Context()) 222 | if err != nil { 223 | http.Error(w, err.Error(), http.StatusInternalServerError) 224 | return 225 | } 226 | 227 | for _, device := range devices { 228 | targets = append(targets, device.ID) 229 | } 230 | } 231 | 232 | registry.MustRegister(deviceLabels) // register global device labels cache 233 | 234 | meterHumidity := prometheus.NewGaugeVec(prometheus.GaugeOpts{ 235 | Namespace: "switchbot", 236 | Subsystem: "meter", 237 | Name: "humidity", 238 | }, []string{"device_id"}) 239 | 240 | meterTemperature := prometheus.NewGaugeVec(prometheus.GaugeOpts{ 241 | Namespace: "switchbot", 242 | Subsystem: "meter", 243 | Name: "temperature", 244 | }, []string{"device_id"}) 245 | meterCO2 := prometheus.NewGaugeVec(prometheus.GaugeOpts{ 246 | Namespace: "switchbot", 247 | Subsystem: "meter", 248 | Name: "CO2", 249 | }, []string{"device_id"}) 250 | plugWeight := prometheus.NewGaugeVec(prometheus.GaugeOpts{ 251 | Namespace: "switchbot", 252 | Subsystem: "plug", 253 | Name: "weight", 254 | }, []string{"device_id"}) 255 | 256 | plugVoltage := prometheus.NewGaugeVec(prometheus.GaugeOpts{ 257 | Namespace: "switchbot", 258 | Subsystem: "plug", 259 | Name: "voltage", 260 | }, []string{"device_id"}) 261 | 262 | plugElectricCurrent := prometheus.NewGaugeVec(prometheus.GaugeOpts{ 263 | Namespace: "switchbot", 264 | Subsystem: "plug", 265 | Name: "electricCurrent", 266 | }, []string{"device_id"}) 267 | 268 | registry.MustRegister(meterHumidity, meterTemperature, meterCO2) 269 | registry.MustRegister(plugWeight, plugVoltage, plugElectricCurrent) 270 | 271 | log.Printf("will try to retrieve metrics for %d devices", len(targets)) 272 | 273 | for _, target := range targets { 274 | log.Printf("getting device status: %s", target) 275 | status, err := h.switchbotClient.Device().Status(r.Context(), target) 276 | if err != nil { 277 | log.Printf("getting device status: %v", err) 278 | return 279 | } 280 | log.Printf("got device status: %s", target) 281 | 282 | switch status.Type { 283 | case switchbot.Meter, switchbot.MeterPlus, switchbot.MeterPro, switchbot.Hub2, switchbot.WoIOSensor, switchbot.Humidifier: 284 | log.Printf("device is a meter-ish device") 285 | 286 | meterHumidity.WithLabelValues(status.ID).Set(float64(status.Humidity)) 287 | meterTemperature.WithLabelValues(status.ID).Set(status.Temperature) 288 | case switchbot.MeterProCO2: 289 | log.Print("device is a CO2 meter") 290 | 291 | meterCO2.WithLabelValues(status.ID).Set(float64(status.CO2)) 292 | meterHumidity.WithLabelValues(status.ID).Set(float64(status.Humidity)) 293 | meterTemperature.WithLabelValues(status.ID).Set(status.Temperature) 294 | case switchbot.PlugMiniJP: 295 | log.Print("device is a plug mini") 296 | 297 | plugWeight.WithLabelValues(status.ID).Set(status.Weight) 298 | plugVoltage.WithLabelValues(status.ID).Set(status.Voltage) 299 | plugElectricCurrent.WithLabelValues(status.ID).Set(status.ElectricCurrent) 300 | default: 301 | log.Printf("unrecognized device type: %s", status.Type) 302 | } 303 | } 304 | 305 | promhttp.HandlerFor(registry, promhttp.HandlerOpts{}).ServeHTTP(w, r) 306 | } 307 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | --------------------------------------------------------------------------------