Skip to content
Merged
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: 24 additions & 0 deletions plugins/inputs/upsd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ plugin ordering. See [CONFIGURATION.md][CONFIGURATION.md] for more details.
## parsed as integers and others as floats.
# force_float = false

## Emit vendor/product IDs as strings regardless of their value. Avoids
## type conflicts when some UPS devices report numeric-looking IDs and
## others report alphanumeric. See README for migration notes.
# stringify_ids = false

## Collect additional fields if they are available for the UPS
## The fields need to be specified as NUT variable names, see
## https://networkupstools.org/docs/developer-guide.chunked/apas02.html
Expand Down Expand Up @@ -71,6 +76,25 @@ Alternatively, you can also map the non-binary value to a `boolean`.

[enum_processor]: /plugins/processors/enum/README.md

### Vendor/Product ID types (`stringify_ids`)

The underlying NUT client library (`go.nut`) auto-detects numeric-looking values
and converts them to `int64`. This means a `vendorid` like `"0764"` becomes
`int64(764)` while a non-numeric `vendorid` like `"ABCD"` stays a string. When
multiple UPS devices of different vendors write to the same InfluxDB bucket,
this causes field type conflicts on `ups_vendorid`, `ups_productid`,
`driver_parameter_vendorid` and `driver_parameter_productid`.

Set `stringify_ids = true` to force these four fields to always be emitted as
strings. The default is currently `false` to preserve backwards-compatible
behavior, but will flip to `true` in a future release. If the option is left
unset, a warning is logged on startup.

> [!NOTE]
> The NUT library parses `"0764"` into `int64(764)` before Telegraf sees it,
> so the stringified value will be `"764"`; leading zeros are lost and cannot
> be recovered at this layer.

## Metrics

This implementation tries to maintain compatibility with the apcupsd metrics:
Expand Down
5 changes: 5 additions & 0 deletions plugins/inputs/upsd/sample.conf
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
## parsed as integers and others as floats.
# force_float = false

## Emit vendor/product IDs as strings regardless of their value. Avoids
## type conflicts when some UPS devices report numeric-looking IDs and
## others report alphanumeric. See README for migration notes.
# stringify_ids = false

## Collect additional fields if they are available for the UPS
## The fields need to be specified as NUT variable names, see
## https://networkupstools.org/docs/developer-guide.chunked/apas02.html
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
upsd,model=CP900EPFCLCD,serial=0,status_OL=true,ups_name=fake battery_charge_low=10i,battery_charge_percent=100i,battery_charge_warning=20i,battery_mfr_date="CPS",battery_runtime=4020i,battery_runtime_low=300i,battery_type="PbAcid",battery_voltage=24,device_mfr="CPS",device_model="CP900EPFCLCD",device_serial=0i,device_type="ups",driver_debug=0i,driver_flag_allow_killpower=0i,driver_name="usbhid-ups",driver_parameter_pollfreq=30i,driver_parameter_pollinterval=2i,driver_parameter_port="auto",driver_parameter_product="CP900EPFCLCD",driver_parameter_productid="ABCD",driver_parameter_serial=0i,driver_parameter_synchronous="auto",driver_parameter_vendor="CPS",driver_parameter_vendorid="EFGH",driver_state="quiet",driver_version="2.8.1",driver_version_data="CyberPower HID 0.8",driver_version_internal=0.52,driver_version_usb="libusb-1.0.26 (API: 0x1000109)",firmware="",input_transfer_high=260i,input_transfer_low=170i,input_voltage=228,load_percent=13i,nominal_battery_voltage=24i,nominal_input_voltage=230i,nominal_power=540i,output_voltage=228,status_flags=8u,time_left_ns=4020000000000i,ups_beeper_status=true,ups_delay_shutdown=20i,ups_delay_start=30i,ups_mfr="CPS",ups_model="CP900EPFCLCD",ups_productid="WXYZ",ups_serial=0i,ups_status="OL",ups_test_result="No test initiated",ups_timer_shutdown=-60i,ups_timer_start=-60i,ups_vendorid="IJKL"
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[[inputs.upsd]]
additional_fields = ["*"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
battery.charge: NUMBER
battery.charge.low: STRING
battery.charge.warning: NUMBER
battery.mfr.date: NUMBER
battery.runtime: NUMBER
battery.runtime.low: STRING
battery.type: NUMBER
battery.voltage: NUMBER
battery.voltage.nominal: NUMBER
device.mfr: NUMBER
device.model: NUMBER
device.serial: NUMBER
device.type: NUMBER
driver.debug: NUMBER
driver.flag.allow_killpower: NUMBER
driver.name: NUMBER
driver.parameter.pollfreq: NUMBER
driver.parameter.pollinterval: NUMBER
driver.parameter.port: NUMBER
driver.parameter.product: NUMBER
driver.parameter.productid: NUMBER
driver.parameter.serial: NUMBER
driver.parameter.synchronous: NUMBER
driver.parameter.vendor: NUMBER
driver.parameter.vendorid: NUMBER
driver.state: NUMBER
driver.version:
driver.version.data: NUMBER
driver.version.internal: NUMBER
driver.version.usb: NUMBER
input.transfer.high: STRING
input.transfer.low: STRING
input.voltage: NUMBER
input.voltage.nominal: NUMBER
output.voltage: NUMBER
ups.beeper.status: NUMBER
ups.delay.shutdown: STRING
ups.delay.start: STRING
ups.load: NUMBER
ups.mfr: NUMBER
ups.model: NUMBER
ups.productid: NUMBER
ups.realpower.nominal: NUMBER
ups.serial: NUMBER
ups.status: NUMBER
ups.test.result: NUMBER
ups.timer.shutdown: NUMBER
ups.timer.start: NUMBER
ups.vendorid: NUMBER
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
battery.charge: 100
battery.charge.low: 10
battery.charge.warning: 20
battery.mfr.date: CPS
battery.runtime: 4020
battery.runtime.low: 300
battery.type: PbAcid
battery.voltage: 24.0
battery.voltage.nominal: 24
device.mfr: CPS
device.model: CP900EPFCLCD
device.serial: 000000000000
device.type: ups
driver.debug: 0
driver.flag.allow_killpower: 0
driver.name: usbhid-ups
driver.parameter.pollfreq: 30
driver.parameter.pollinterval: 2
driver.parameter.port: auto
driver.parameter.product: CP900EPFCLCD
driver.parameter.productid: ABCD
driver.parameter.serial: 000000000000
driver.parameter.synchronous: auto
driver.parameter.vendor: CPS
driver.parameter.vendorid: EFGH
driver.state: quiet
driver.version: 2.8.1
driver.version.data: CyberPower HID 0.8
driver.version.internal: 0.52
driver.version.usb: libusb-1.0.26 (API: 0x1000109)
input.transfer.high: 260
input.transfer.low: 170
input.voltage: 228.0
input.voltage.nominal: 230
output.voltage: 228.0
ups.beeper.status: enabled
ups.delay.shutdown: 20
ups.delay.start: 30
ups.load: 13
ups.mfr: CPS
ups.model: CP900EPFCLCD
ups.productid: WXYZ
ups.realpower.nominal: 540
ups.serial: 000000000000
ups.status: OL
ups.test.result: No test initiated
ups.timer.shutdown: -60
ups.timer.start: -60
ups.vendorid: IJKL
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
upsd,model=CP900EPFCLCD,serial=0,status_OL=true,ups_name=fake battery_charge_low=10i,battery_charge_percent=100i,battery_charge_warning=20i,battery_mfr_date="CPS",battery_runtime=4020i,battery_runtime_low=300i,battery_type="PbAcid",battery_voltage=24,device_mfr="CPS",device_model="CP900EPFCLCD",device_serial=0i,device_type="ups",driver_debug=0i,driver_flag_allow_killpower=0i,driver_name="usbhid-ups",driver_parameter_pollfreq=30i,driver_parameter_pollinterval=2i,driver_parameter_port="auto",driver_parameter_product="CP900EPFCLCD",driver_parameter_productid="501",driver_parameter_serial=0i,driver_parameter_synchronous="auto",driver_parameter_vendor="CPS",driver_parameter_vendorid="764",driver_state="quiet",driver_version="2.8.1",driver_version_data="CyberPower HID 0.8",driver_version_internal=0.52,driver_version_usb="libusb-1.0.26 (API: 0x1000109)",firmware="",input_transfer_high=260i,input_transfer_low=170i,input_voltage=228,load_percent=13i,nominal_battery_voltage=24i,nominal_input_voltage=230i,nominal_power=540i,output_voltage=228,status_flags=8u,time_left_ns=4020000000000i,ups_beeper_status=true,ups_delay_shutdown=20i,ups_delay_start=30i,ups_mfr="CPS",ups_model="CP900EPFCLCD",ups_productid="501",ups_serial=0i,ups_status="OL",ups_test_result="No test initiated",ups_timer_shutdown=-60i,ups_timer_start=-60i,ups_vendorid="764"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[[inputs.upsd]]
additional_fields = ["*"]
stringify_ids = true
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
battery.charge: NUMBER
battery.charge.low: STRING
battery.charge.warning: NUMBER
battery.mfr.date: NUMBER
battery.runtime: NUMBER
battery.runtime.low: STRING
battery.type: NUMBER
battery.voltage: NUMBER
battery.voltage.nominal: NUMBER
device.mfr: NUMBER
device.model: NUMBER
device.serial: NUMBER
device.type: NUMBER
driver.debug: NUMBER
driver.flag.allow_killpower: NUMBER
driver.name: NUMBER
driver.parameter.pollfreq: NUMBER
driver.parameter.pollinterval: NUMBER
driver.parameter.port: NUMBER
driver.parameter.product: NUMBER
driver.parameter.productid: NUMBER
driver.parameter.serial: NUMBER
driver.parameter.synchronous: NUMBER
driver.parameter.vendor: NUMBER
driver.parameter.vendorid: NUMBER
driver.state: NUMBER
driver.version:
driver.version.data: NUMBER
driver.version.internal: NUMBER
driver.version.usb: NUMBER
input.transfer.high: STRING
input.transfer.low: STRING
input.voltage: NUMBER
input.voltage.nominal: NUMBER
output.voltage: NUMBER
ups.beeper.status: NUMBER
ups.delay.shutdown: STRING
ups.delay.start: STRING
ups.load: NUMBER
ups.mfr: NUMBER
ups.model: NUMBER
ups.productid: NUMBER
ups.realpower.nominal: NUMBER
ups.serial: NUMBER
ups.status: NUMBER
ups.test.result: NUMBER
ups.timer.shutdown: NUMBER
ups.timer.start: NUMBER
ups.vendorid: NUMBER
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
battery.charge: 100
battery.charge.low: 10
battery.charge.warning: 20
battery.mfr.date: CPS
battery.runtime: 4020
battery.runtime.low: 300
battery.type: PbAcid
battery.voltage: 24.0
battery.voltage.nominal: 24
device.mfr: CPS
device.model: CP900EPFCLCD
device.serial: 000000000000
device.type: ups
driver.debug: 0
driver.flag.allow_killpower: 0
driver.name: usbhid-ups
driver.parameter.pollfreq: 30
driver.parameter.pollinterval: 2
driver.parameter.port: auto
driver.parameter.product: CP900EPFCLCD
driver.parameter.productid: 0501
driver.parameter.serial: 000000000000
driver.parameter.synchronous: auto
driver.parameter.vendor: CPS
driver.parameter.vendorid: 0764
driver.state: quiet
driver.version: 2.8.1
driver.version.data: CyberPower HID 0.8
driver.version.internal: 0.52
driver.version.usb: libusb-1.0.26 (API: 0x1000109)
input.transfer.high: 260
input.transfer.low: 170
input.voltage: 228.0
input.voltage.nominal: 230
output.voltage: 228.0
ups.beeper.status: enabled
ups.delay.shutdown: 20
ups.delay.start: 30
ups.load: 13
ups.mfr: CPS
ups.model: CP900EPFCLCD
ups.productid: 0501
ups.realpower.nominal: 540
ups.serial: 000000000000
ups.status: OL
ups.test.result: No test initiated
ups.timer.shutdown: -60
ups.timer.start: -60
ups.vendorid: 0764
47 changes: 38 additions & 9 deletions plugins/inputs/upsd/upsd.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ import (
var sampleConfig string

var (
// Fields that must always be emitted as strings to avoid type conflicts.
// The go.nut library auto-detects numeric-looking values (e.g. "0764")
// and converts them to int64, but IDs like vendorid/productid should
// remain strings regardless of their content.
stringFieldSet = map[string]bool{
"ups.vendorid": true,
"ups.productid": true,
"driver.parameter.vendorid": true,
"driver.parameter.productid": true,
}

// Define the default field set to add if existing
defaultFieldSet = map[string]string{
"battery.charge": "battery_charge_percent",
Expand Down Expand Up @@ -46,14 +57,15 @@ const (
)

type Upsd struct {
Server string `toml:"server"`
Port int `toml:"port"`
Username string `toml:"username"`
Password string `toml:"password"`
ForceFloat bool `toml:"force_float"`
Additional []string `toml:"additional_fields"`
DumpRaw bool `toml:"dump_raw_variables" deprecated:"1.35.0;use 'log_level' 'trace' instead"`
Log telegraf.Logger `toml:"-"`
Server string `toml:"server"`
Port int `toml:"port"`
Username string `toml:"username"`
Password string `toml:"password"`
ForceFloat bool `toml:"force_float"`
StringifyIDs *bool `toml:"stringify_ids"`
Additional []string `toml:"additional_fields"`
DumpRaw bool `toml:"dump_raw_variables" deprecated:"1.35.0;use 'log_level' 'trace' instead"`
Log telegraf.Logger `toml:"-"`

filter filter.Filter
dumped map[string]bool
Expand All @@ -73,6 +85,12 @@ func (u *Upsd) Init() error {

u.dumped = make(map[string]bool)

if u.StringifyIDs == nil {
u.Log.Warn("Option 'stringify_ids' is not set; the default will flip from 'false' to 'true' " +
"in a future release to fix vendor/product ID type conflicts. " +
"Set it explicitly to lock in behavior and silence this warning.")
}

return nil
}

Expand Down Expand Up @@ -168,8 +186,19 @@ func (u *Upsd) gatherUps(acc telegraf.Accumulator, upsname string, variables []n
continue
}

// Force ID fields to always be strings to avoid type conflicts
// between UPS devices with numeric-looking IDs (auto-converted to
// int64 by go.nut) and devices with non-numeric IDs.
stringify := u.StringifyIDs != nil && *u.StringifyIDs && stringFieldSet[varname]
if stringify {
str, err := internal.ToString(v)
if err == nil {
v = str
}
}

// Force expected float values to actually being float (e.g. if delivered as int)
if u.ForceFloat {
if !stringify && u.ForceFloat {
float, err := internal.ToFloat64(v)
if err == nil {
v = float
Expand Down
36 changes: 36 additions & 0 deletions plugins/inputs/upsd/upsd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,41 @@ import (
"github.com/influxdata/telegraf/testutil"
)

func TestStringifyIDsWarning(t *testing.T) {
tr := true
fa := false
tests := []struct {
name string
value *bool
wantWarning bool
}{
{name: "unset emits warning", value: nil, wantWarning: true},
{name: "explicit true is silent", value: &tr, wantWarning: false},
{name: "explicit false is silent", value: &fa, wantWarning: false},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
logger := &testutil.CaptureLogger{}
plugin := &Upsd{
Server: defaultAddress,
Port: defaultPort,
StringifyIDs: tc.value,
Log: logger,
}
require.NoError(t, plugin.Init())

warnings := logger.Warnings()
if tc.wantWarning {
require.Len(t, warnings, 1)
require.Contains(t, warnings[0], "stringify_ids")
} else {
require.Empty(t, warnings)
}
})
}
}

func TestBadServer(t *testing.T) {
// Create and start a server without interactions
server := &mockServer{}
Expand All @@ -33,6 +68,7 @@ func TestBadServer(t *testing.T) {
plugin := &Upsd{
Server: addr.IP.String(),
Port: addr.Port,
Log: testutil.Logger{},
}
require.NoError(t, plugin.Init())

Expand Down
Loading