Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Tripp Lite AVR650UM. Add an OpenWRT init script. Add … #1

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,25 @@

This is a simple Prometheus exporter for Tripp Lite UPS devices that expose their properties as a USB HID device.

As of today this has only been tested on a SMART1500LCD with USB vendor ID 09AE and product ID 2012. It has only been tested on Linux (and specifically a Raspberry Pi) though it should also work on Mac and Windows thanks to the [Gopher Interface Devices](https://github.com/karalabe/hid) HID library it uses.
As of today this has only been tested on Tripp Lite devices with USB vendor ID 09AE, including a SMART1500LCD with product ID 2012, an AVR900U and an AVR650UM, both with product ID 3024. It has only been tested on Linux (a Raspberry Pi, an Intel NUC (native build), a Linksys WRT3200ACM (native build), and a VoCore2 (cross-compiled)) though it should also work on Mac and Windows thanks to the [Gopher Interface Devices](https://github.com/karalabe/hid) HID library it uses.

## Cross Compiling

As this is written in Go, cross-compilation is logically simple, and the preferred method for building when the target OS is OpenWrt Linux. However, the C package used to access USB devices has to be compiled for the target platform, otherwise the UPS will never be detected. For OpenWrt, set up a toolchain as described in https://openwrt.org/docs/guide-developer/toolchain/use-buildsystem up to making the kernel_menuconfig. This will generate the staging_dir with the needed gcc compiler.

The OpenWrt `git clone` directory as used below is `$HOME/Source/External/openwrt`, so update the export line as needed to point the directory used and the toolchain version. The below:

export STAGING_DIR=$HOME/Source/External/openwrt/staging_dir/toolchain-mipsel_24kc_gcc-11.2.0_musl
CGO_ENABLED=1 CC=$STAGING_DIR/bin/mipsel-openwrt-linux-musl-gcc GOOS=linux GOARCH=mipsle GOMIPS=softfloat go get -a -ldflags '-w' github.com/karalabe/hid
CGO_ENABLED=1 CC=$STAGING_DIR/bin/mipsel-openwrt-linux-musl-gcc GOOS=linux GOARCH=mipsle GOMIPS=softfloat go build -a -ldflags '-w -extldflags -static' -o tripplite-ups-exporter-mipsel main.go

will generate a static executable suitable for running on a VoCore2 running OpenWRT 22.03. The below generates a static executable suitable for running on a Linksys WRT3200ACM, also running running OpenWRT.

export STAGING_DIR=$HOME/Source/External/openwrt/staging_dir/toolchain-arm_cortex-a9+vfpv3-d16_gcc-11.2.0_musl_eabi
CGO_ENABLED=1 CC=$STAGING_DIR/bin/arm-openwrt-linux-muslgnueabi-gcc GOOS=linux GOARCH=arm go get -a -ldflags '-w' github.com/karalabe/hid
CGO_ENABLED=1 CC=$STAGING_DIR/bin/arm-openwrt-linux-muslgnueabi-gcc GOOS=linux GOARCH=arm go build -a -ldflags '-w -extldflags -static' -o tripplite-ups-exporter-arm7 main.go

See https://go.dev/doc/install/source#environment for the list of valid combinations and additional CPU specific options (like GOMIPS as above).

## Installing

Expand All @@ -26,6 +44,10 @@ Then you can run the service:

By default the exporter will listen on port 9528. This can be changed with the `-addr` flag.

### OpenWRT init script

The prometheus-tripplite-ups-exporter script goes in /etc/init.d on OpenWRT to enable automatic startup on boot. The script expects the appropriate binary at /usr/bin/prometheus-tripplite-ups-exporter.

## Contributing

Issues and pull requests are welcome. When filing a PR, please make sure the code has been run through `gofmt`.
Expand Down
243 changes: 122 additions & 121 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,147 +1,148 @@
package main

import (
"flag"
"fmt"
"io"
"net/http"
"os"
"time"

"github.com/karalabe/hid"
)

const (
vendorID = 0x09ae
productID = 0x2012
"flag"
"fmt"
"io"
"net/http"
"os"
"time"

"github.com/karalabe/hid"
)

type formatFunc func(data []byte, d *hid.Device, name string, w io.Writer)

type feature struct {
name string
reportID byte
length int
format formatFunc
name string
reportID byte
length int
format formatFunc
}

func intFormatter(data []byte, d *hid.Device, name string, w io.Writer) {
var v uint64
for i, b := range data {
v |= uint64(b) << (8 * i)
}
var v uint64
for i, b := range data {
v |= uint64(b) << (8 * i)
}

fmt.Fprintf(w, "# TYPE %s gauge\n", name)
fmt.Fprintf(w, "%s{path=\"%s\",product=\"%s\"} %d\n", name, d.Path, d.Product, v)
fmt.Fprintf(w, "# TYPE %s gauge\n", name)
fmt.Fprintf(w, "%s{path=\"%s\",product=\"%s\"} %d\n", name, d.Path, d.Product, v)
}

func oneTenthFloatFormatter(data []byte, d *hid.Device, name string, w io.Writer) {
var v uint64
for i, b := range data {
v |= uint64(b) << (8 * i)
}

fmt.Fprintf(w, "# TYPE %s gauge\n", name)
fmt.Fprintf(w, "%s{path=\"%s\",product=\"%s\"} %f\n", name, d.Path, d.Product, float64(v)/10.0)
var v uint64
for i, b := range data {
v |= uint64(b) << (8 * i)
}

fmt.Fprintf(w, "# TYPE %s gauge\n", name)
fmt.Fprintf(w, "%s{path=\"%s\",product=\"%s\"} %f\n", name, d.Path, d.Product, float64(v)/10.0)
}

func statusFormatter(data []byte, d *hid.Device, name string, w io.Writer) {
bitOn := func(b byte, i int) int {
if b&(1<<i) != 0 {
return 1
} else {
return 0
}
}

fields := []string{
"shutdown_imminent",
"ac_present",
"charging",
"discharging",
"needs_replacement",
"below_remaining_capacity",
"fully_charged",
"fully_discharged",
}

for i, f := range fields {
n := name + "_" + f

fmt.Fprintf(w, "# TYPE %s gauge\n", n)
fmt.Fprintf(w, "%s{path=\"%s\",product=\"%s\"} %d\n", n, d.Path, d.Product, bitOn(data[0], i))
}
bitOn := func(b byte, i int) int {
if b&(1<<i) != 0 {
return 1
} else {
return 0
}
}

fields := []string{
"shutdown_imminent",
"ac_present",
"charging",
"discharging",
"needs_replacement",
"below_remaining_capacity",
"fully_charged",
"fully_discharged",
}

for i, f := range fields {
n := name + "_" + f

fmt.Fprintf(w, "# TYPE %s gauge\n", n)
fmt.Fprintf(w, "%s{path=\"%s\",product=\"%s\"} %d\n", n, d.Path, d.Product, bitOn(data[0], i))
}
}

var features = []feature{
{"tripplite_config_voltage", 48, 1, intFormatter},
{"tripplite_config_frequency_hz", 2, 1, intFormatter},
{"tripplite_config_power_watts", 3, 2, intFormatter},
{"tripplite_input_voltage", 24, 2, oneTenthFloatFormatter},
{"tripplite_input_frequency_hz", 25, 2, oneTenthFloatFormatter},
{"tripplite_output_voltage", 27, 2, oneTenthFloatFormatter},
{"tripplite_output_power_watts", 71, 2, intFormatter},
{"tripplite_current_charge_pct", 52, 1, intFormatter},
{"tripplite_run_time_to_empty_minutes", 53, 2, intFormatter},
{"tripplite_status", 50, 1, statusFormatter},
{"tripplite_config_voltage", 48, 1, intFormatter},
{"tripplite_config_frequency_hz", 2, 1, intFormatter},
{"tripplite_config_power_watts", 3, 2, intFormatter},
{"tripplite_input_voltage", 24, 2, oneTenthFloatFormatter},
{"tripplite_input_frequency_hz", 25, 2, oneTenthFloatFormatter},
{"tripplite_output_voltage", 27, 2, oneTenthFloatFormatter},
{"tripplite_output_power_watts", 71, 2, intFormatter},
{"tripplite_current_charge_pct", 52, 1, intFormatter},
{"tripplite_run_time_to_empty_minutes", 53, 2, intFormatter},
{"tripplite_status", 50, 1, statusFormatter},
}

const vendorID uint16 = 0x09ae

var productID = [2]uint16{0x2012, 0x3024}

func main() {
var addr string

flag.StringVar(&addr, "addr", ":9528", "Prometheus exporter listen address")
flag.Parse()

var dinfos []hid.DeviceInfo
for len(dinfos) == 0 {
dinfos = hid.Enumerate(vendorID, productID)
if len(dinfos) == 0 {
fmt.Println("No devices found, waiting 5 seconds")
time.Sleep(5 * time.Second)
}
}

var devices []*hid.Device
for _, di := range dinfos {
d, err := di.Open()
if err != nil {
fmt.Println(err)
continue
}

devices = append(devices, d)
}

mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/metrics", http.StatusMovedPermanently)
})
mux.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
for _, d := range devices {
for _, f := range features {
data := make([]byte, f.length+1)
data[0] = f.reportID

_, err := d.GetFeatureReport(data)
if err != nil {
fmt.Println(err)
continue
}

f.format(data[1:], d, f.name, w)
}
}
})

s := http.Server{
Addr: addr,
Handler: mux,
}

fmt.Println("Starting Prometheus exporter on", addr)
if err := s.ListenAndServe(); err != nil {
fmt.Printf("Unable to run HTTP server: %v", err)
os.Exit(1)
}
var addr string

flag.StringVar(&addr, "addr", ":9528", "Prometheus exporter listen address")
flag.Parse()

var dinfos []hid.DeviceInfo
for len(dinfos) == 0 {
for ii := 0; ii < len(productID); ii++ {
dinfos = hid.Enumerate(vendorID, productID[ii])
if len(dinfos) != 0 {
break
}
fmt.Println("No devices found, waiting 5 seconds")
time.Sleep(5 * time.Second)
}
}

var devices []*hid.Device
for _, di := range dinfos {
d, err := di.Open()
if err != nil {
fmt.Println(err)
continue
}

devices = append(devices, d)
}

mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/metrics", http.StatusMovedPermanently)
})
mux.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
for _, d := range devices {
for _, f := range features {
data := make([]byte, f.length+1)
data[0] = f.reportID

_, err := d.GetFeatureReport(data)
if err != nil {
fmt.Println(err)
continue
}

f.format(data[1:], d, f.name, w)
}
}
})

s := http.Server{
Addr: addr,
Handler: mux,
}

fmt.Println("Starting Prometheus exporter on", addr)
if err := s.ListenAndServe(); err != nil {
fmt.Printf("Unable to run HTTP server: %v", err)
os.Exit(1)
}
}
12 changes: 12 additions & 0 deletions prometheus-tripplite-ups-exporter
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/sh /etc/rc.common
# Copyright (C) 2006 OpenWrt.org, 2022 Alan Brenner

# shellcheck disable=SC2034
START=61
USE_PROCD=1

start_service() {
procd_open_instance
procd_set_param command /usr/bin/prometheus-tripplite-ups-exporter
procd_close_instance
}