189 lines
5.0 KiB
Go
189 lines
5.0 KiB
Go
|
|
package upsd
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"github.com/influxdata/telegraf"
|
||
|
|
"github.com/influxdata/telegraf/internal/choice"
|
||
|
|
"github.com/influxdata/telegraf/plugins/inputs"
|
||
|
|
nut "github.com/robbiet480/go.nut"
|
||
|
|
"strings"
|
||
|
|
)
|
||
|
|
|
||
|
|
//See: https://networkupstools.org/docs/developer-guide.chunked/index.html
|
||
|
|
|
||
|
|
const defaultAddress = "127.0.0.1"
|
||
|
|
const defaultPort = 3493
|
||
|
|
|
||
|
|
type Upsd struct {
|
||
|
|
Server string
|
||
|
|
Port int
|
||
|
|
Username string
|
||
|
|
Password string
|
||
|
|
Log telegraf.Logger `toml:"-"`
|
||
|
|
|
||
|
|
batteryRuntimeTypeWarningIssued bool
|
||
|
|
}
|
||
|
|
|
||
|
|
func (*Upsd) Description() string {
|
||
|
|
return "Monitor UPSes connected via Network UPS Tools"
|
||
|
|
}
|
||
|
|
|
||
|
|
var sampleConfig = `
|
||
|
|
## A running NUT server to connect to.
|
||
|
|
# server = "127.0.0.1"
|
||
|
|
# port = 3493
|
||
|
|
# username = "user"
|
||
|
|
# password = "password"
|
||
|
|
`
|
||
|
|
|
||
|
|
func (*Upsd) SampleConfig() string {
|
||
|
|
return sampleConfig
|
||
|
|
}
|
||
|
|
|
||
|
|
func (u *Upsd) Gather(acc telegraf.Accumulator) error {
|
||
|
|
upsList, err := u.fetchVariables(u.Server, u.Port)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
for name, variables := range upsList {
|
||
|
|
u.gatherUps(acc, name, variables)
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (u *Upsd) gatherUps(acc telegraf.Accumulator, name string, variables []nut.Variable) {
|
||
|
|
metrics := make(map[string]interface{})
|
||
|
|
for _, variable := range variables {
|
||
|
|
name := variable.Name
|
||
|
|
value := variable.Value
|
||
|
|
metrics[name] = value
|
||
|
|
}
|
||
|
|
|
||
|
|
tags := map[string]string{
|
||
|
|
"serial": fmt.Sprintf("%v", metrics["device.serial"]),
|
||
|
|
"ups_name": name,
|
||
|
|
//"variables": variables.Status not sure if it's a good idea to provide this
|
||
|
|
"model": fmt.Sprintf("%v", metrics["device.model"]),
|
||
|
|
}
|
||
|
|
|
||
|
|
// For compatibility with the apcupsd plugin's output we map the status string status into a bit-format
|
||
|
|
status := u.mapStatus(metrics, tags)
|
||
|
|
|
||
|
|
timeLeftS, ok := metrics["battery.runtime"].(int64)
|
||
|
|
if !ok && !u.batteryRuntimeTypeWarningIssued {
|
||
|
|
u.Log.Warnf("'battery.runtime' type is not int64")
|
||
|
|
u.batteryRuntimeTypeWarningIssued = true
|
||
|
|
}
|
||
|
|
|
||
|
|
fields := map[string]interface{}{
|
||
|
|
"status_flags": status,
|
||
|
|
"ups_status": metrics["ups.status"],
|
||
|
|
"input_voltage": metrics["input.voltage"],
|
||
|
|
"load_percent": metrics["ups.load"],
|
||
|
|
"battery_charge_percent": metrics["battery.charge"],
|
||
|
|
"time_left_ns": timeLeftS * 1_000_000_000, //Compatibility with apcupsd metrics format
|
||
|
|
"output_voltage": metrics["output.voltage"],
|
||
|
|
"internal_temp": metrics["ups.temperature"],
|
||
|
|
"battery_voltage": metrics["battery.voltage"],
|
||
|
|
"input_frequency": metrics["input.frequency"],
|
||
|
|
"nominal_input_voltage": metrics["input.voltage.nominal"],
|
||
|
|
"nominal_battery_voltage": metrics["battery.voltage.nominal"],
|
||
|
|
"nominal_power": metrics["ups.realpower.nominal"],
|
||
|
|
"firmware": metrics["ups.firmware"],
|
||
|
|
"battery_date": metrics["battery.mfr.date"],
|
||
|
|
}
|
||
|
|
|
||
|
|
acc.AddFields("upsd", fields, tags)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (u *Upsd) mapStatus(metrics map[string]interface{}, tags map[string]string) uint64 {
|
||
|
|
status := uint64(0)
|
||
|
|
statusString := fmt.Sprintf("%v", metrics["ups.status"])
|
||
|
|
statuses := strings.Fields(statusString)
|
||
|
|
//Source: 1.3.2 at http://rogerprice.org/NUT/ConfigExamples.A5.pdf
|
||
|
|
//apcupsd bits:
|
||
|
|
//0 Runtime calibration occurring (Not reported by Smart UPS v/s and BackUPS Pro)
|
||
|
|
//1 SmartTrim (Not reported by 1st and 2nd generation SmartUPS models)
|
||
|
|
//2 SmartBoost
|
||
|
|
//3 On line (this is the normal condition)
|
||
|
|
//4 On battery
|
||
|
|
//5 Overloaded output
|
||
|
|
//6 Battery low
|
||
|
|
//7 Replace battery
|
||
|
|
if choice.Contains("CAL", statuses) {
|
||
|
|
status |= 1 << 0
|
||
|
|
tags["status_CAL"] = "true"
|
||
|
|
}
|
||
|
|
if choice.Contains("TRIM", statuses) {
|
||
|
|
status |= 1 << 1
|
||
|
|
tags["status_TRIM"] = "true"
|
||
|
|
}
|
||
|
|
if choice.Contains("BOOST", statuses) {
|
||
|
|
status |= 1 << 2
|
||
|
|
tags["status_BOOST"] = "true"
|
||
|
|
}
|
||
|
|
if choice.Contains("OL", statuses) {
|
||
|
|
status |= 1 << 3
|
||
|
|
tags["status_OL"] = "true"
|
||
|
|
}
|
||
|
|
if choice.Contains("OB", statuses) {
|
||
|
|
status |= 1 << 4
|
||
|
|
tags["status_OB"] = "true"
|
||
|
|
}
|
||
|
|
if choice.Contains("OVER", statuses) {
|
||
|
|
status |= 1 << 5
|
||
|
|
tags["status_OVER"] = "true"
|
||
|
|
}
|
||
|
|
if choice.Contains("LB", statuses) {
|
||
|
|
status |= 1 << 6
|
||
|
|
tags["status_LB"] = "true"
|
||
|
|
}
|
||
|
|
if choice.Contains("RB", statuses) {
|
||
|
|
status |= 1 << 7
|
||
|
|
tags["status_RB"] = "true"
|
||
|
|
}
|
||
|
|
return status
|
||
|
|
}
|
||
|
|
|
||
|
|
func (u *Upsd) fetchVariables(server string, port int) (map[string][]nut.Variable, error) {
|
||
|
|
client, err := nut.Connect(server, port)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("connect: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
if u.Username != "" && u.Password != "" {
|
||
|
|
_, err = client.Authenticate(u.Username, u.Password)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("auth: %w", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
upsList, err := client.GetUPSList()
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("getupslist: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
defer func() {
|
||
|
|
_, disconnectErr := client.Disconnect()
|
||
|
|
if disconnectErr != nil {
|
||
|
|
err = fmt.Errorf("disconnect: %w", disconnectErr)
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
|
||
|
|
result := make(map[string][]nut.Variable)
|
||
|
|
for _, ups := range upsList {
|
||
|
|
result[ups.Name] = ups.Variables
|
||
|
|
}
|
||
|
|
|
||
|
|
return result, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func init() {
|
||
|
|
inputs.Add("upsd", func() telegraf.Input {
|
||
|
|
return &Upsd{
|
||
|
|
Server: defaultAddress,
|
||
|
|
Port: defaultPort,
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|