add go.d/apcupsd (#18489)
Ilya Mashchenko committed
Sep 8, 2024 at 20:14 UTC
6c33a0d2367191cfcff4fc4fdc550322b607e8c2
18 files changed
+1412
-25
src/go/plugin/go.d/README.md
+1
@@ -53,6 +53,7 @@ see the appropriate collector readme.
53
| [activemq](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/activemq) | ActiveMQ |
54
| [ap](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/ap) | Wireless AP |
55
| [apache](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/apache) | Apache |
56
+| [apcupsd](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/apcupsd) | UPS (APC) |
57
| [beanstalk](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/beanstalk) | Beanstalk |
58
| [bind](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/bind) | ISC Bind |
59
| [boinc](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/boinc) | BOINC |
src/go/plugin/go.d/config/go.d.conf
+1
@@ -19,6 +19,7 @@ modules:
19
# activemq: yes
20
# ap: yes
21
# apache: yes
22
+# apcupsd: yes
23
# beanstalk: yes
24
# bind: yes
25
# boinc: yes
src/go/plugin/go.d/config/go.d/apcupsd.conf
new
+6
@@ -0,0 +1,6 @@
1
+## All available configuration options, their descriptions and default values:
2
+## https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/apcupsd#readme
3
+
4
+#jobs:
5
+# - name: local
6
+# address: 127.0.0.1:3551
src/go/plugin/go.d/config/go.d/sd/net_listeners.conf
+7
@@ -16,6 +16,8 @@ classify:
16
expr: '{{ and (eq .Port "8161") (eq .Comm "activemq") }}'
17
- tags: "apache"
18
expr: '{{ and (eq .Port "80" "8080") (eq .Comm "apache" "apache2" "httpd") }}'
19
+ - tags: "apcupsd"
20
+ expr: '{{ or (eq .Port "3551") (eq .Comm "apcupsd") }}'
21
- tags: "beanstalk"
22
expr: '{{ or (eq .Port "11300") (eq .Comm "beanstalkd") }}'
23
- tags: "boinc"
@@ -159,6 +161,11 @@ compose:
161
module: apache
162
name: local
163
url: http://{{.Address}}/server-status?auto
164
+ - selector: "apcupsd"
165
+ template: |
166
+ module: apcupsd
167
+ name: local_{{.Port}}
168
+ address: {{.Address}}
169
- selector: "beanstalk"
170
template: |
171
module: beanstalk
src/go/plugin/go.d/modules/apcupsd/apcupsd.go
new
+100
@@ -0,0 +1,100 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package apcupsd
4
+
5
+import (
6
+ _ "embed"
7
+ "errors"
8
+ "time"
9
+
10
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
11
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web"
12
+)
13
+
14
+//go:embed "config_schema.json"
15
+var configSchema string
16
+
17
+func init() {
18
+ module.Register("apcupsd", module.Creator{
19
+ JobConfigSchema: configSchema,
20
+ Create: func() module.Module { return New() },
21
+ Config: func() any { return &Config{} },
22
+ })
23
+}
24
+
25
+func New() *Apcupsd {
26
+ return &Apcupsd{
27
+ Config: Config{
28
+ Address: "127.0.0.1:3551",
29
+ Timeout: web.Duration(time.Second * 3),
30
+ },
31
+ newConn: newUpsdConn,
32
+ charts: charts.Copy(),
33
+ }
34
+}
35
+
36
+type Config struct {
37
+ UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
38
+ Address string `yaml:"address" json:"address"`
39
+ Timeout web.Duration `yaml:"timeout,omitempty" json:"timeout"`
40
+}
41
+
42
+type Apcupsd struct {
43
+ module.Base
44
+ Config `yaml:",inline" json:""`
45
+
46
+ charts *module.Charts
47
+
48
+ conn apcupsdConn
49
+ newConn func(Config) apcupsdConn
50
+}
51
+
52
+func (a *Apcupsd) Configuration() any {
53
+ return a.Config
54
+}
55
+
56
+func (a *Apcupsd) Init() error {
57
+ if a.Address == "" {
58
+ a.Error("config: 'address' not set")
59
+ return errors.New("address not set")
60
+ }
61
+
62
+ return nil
63
+}
64
+
65
+func (a *Apcupsd) Check() error {
66
+ mx, err := a.collect()
67
+ if err != nil {
68
+ a.Error(err)
69
+ return err
70
+ }
71
+ if len(mx) == 0 {
72
+ return errors.New("no metrics collected")
73
+ }
74
+ return nil
75
+}
76
+
77
+func (a *Apcupsd) Charts() *module.Charts {
78
+ return a.charts
79
+}
80
+
81
+func (a *Apcupsd) Collect() map[string]int64 {
82
+ mx, err := a.collect()
83
+ if err != nil {
84
+ a.Error(err)
85
+ }
86
+
87
+ if len(mx) == 0 {
88
+ return nil
89
+ }
90
+ return mx
91
+}
92
+
93
+func (a *Apcupsd) Cleanup() {
94
+ if a.conn != nil {
95
+ if err := a.conn.disconnect(); err != nil {
96
+ a.Warningf("error on disconnect: %v", err)
97
+ }
98
+ a.conn = nil
99
+ }
100
+}
src/go/plugin/go.d/modules/apcupsd/apcupsd_test.go
new
+283
@@ -0,0 +1,283 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package apcupsd
4
+
5
+import (
6
+ "errors"
7
+ "os"
8
+ "strings"
9
+ "testing"
10
+
11
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
12
+
13
+ "github.com/stretchr/testify/assert"
14
+ "github.com/stretchr/testify/require"
15
+)
16
+
17
+var (
18
+ dataConfigJSON, _ = os.ReadFile("testdata/config.json")
19
+ dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
20
+
21
+ dataStatus, _ = os.ReadFile("testdata/status.txt")
22
+ dataStatusCommlost, _ = os.ReadFile("testdata/status_commlost.txt")
23
+)
24
+
25
+func Test_testDataIsValid(t *testing.T) {
26
+ for name, data := range map[string][]byte{
27
+ "dataConfigJSON": dataConfigJSON,
28
+ "dataConfigYAML": dataConfigYAML,
29
+ "dataStatus": dataStatus,
30
+ "dataStatusCommlost": dataStatusCommlost,
31
+ } {
32
+ require.NotNil(t, data, name)
33
+ }
34
+}
35
+
36
+func TestApcupsd_ConfigurationSerialize(t *testing.T) {
37
+ module.TestConfigurationSerialize(t, &Apcupsd{}, dataConfigJSON, dataConfigYAML)
38
+}
39
+
40
+func TestApcupsd_Cleanup(t *testing.T) {
41
+ apc := New()
42
+
43
+ require.NotPanics(t, apc.Cleanup)
44
+
45
+ mock := prepareMockOk()
46
+ apc.newConn = func(Config) apcupsdConn { return mock }
47
+
48
+ require.NoError(t, apc.Init())
49
+ _ = apc.Collect()
50
+ require.NotPanics(t, apc.Cleanup)
51
+ assert.True(t, mock.calledDisconnect)
52
+}
53
+
54
+func TestApcupsd_Init(t *testing.T) {
55
+ tests := map[string]struct {
56
+ config Config
57
+ wantFail bool
58
+ }{
59
+ "success on default config": {
60
+ wantFail: false,
61
+ config: New().Config,
62
+ },
63
+ "fails when 'address' option not set": {
64
+ wantFail: true,
65
+ config: Config{Address: ""},
66
+ },
67
+ }
68
+
69
+ for name, test := range tests {
70
+ t.Run(name, func(t *testing.T) {
71
+ apc := New()
72
+ apc.Config = test.config
73
+
74
+ if test.wantFail {
75
+ assert.Error(t, apc.Init())
76
+ } else {
77
+ assert.NoError(t, apc.Init())
78
+ }
79
+ })
80
+ }
81
+}
82
+
83
+func TestApcupsd_Check(t *testing.T) {
84
+ tests := map[string]struct {
85
+ prepareMock func() *mockApcupsdConn
86
+ wantFail bool
87
+ }{
88
+ "case ok": {
89
+ wantFail: false,
90
+ prepareMock: prepareMockOk,
91
+ },
92
+ "case commlost": {
93
+ wantFail: false,
94
+ prepareMock: prepareMockOkCommlost,
95
+ },
96
+ "error on connect()": {
97
+ wantFail: true,
98
+ prepareMock: prepareMockErrOnConnect,
99
+ },
100
+ "error on status()": {
101
+ wantFail: true,
102
+ prepareMock: prepareMockErrOnStatus,
103
+ },
104
+ }
105
+
106
+ for name, test := range tests {
107
+ t.Run(name, func(t *testing.T) {
108
+ apc := New()
109
+ apc.newConn = func(Config) apcupsdConn { return test.prepareMock() }
110
+
111
+ require.NoError(t, apc.Init())
112
+
113
+ if test.wantFail {
114
+ assert.Error(t, apc.Check())
115
+ } else {
116
+ assert.NoError(t, apc.Check())
117
+ }
118
+ })
119
+ }
120
+}
121
+
122
+func TestApcupsd_Charts(t *testing.T) {
123
+ apc := New()
124
+ require.NoError(t, apc.Init())
125
+ assert.NotNil(t, apc.Charts())
126
+}
127
+
128
+func TestApcupsd_Collect(t *testing.T) {
129
+ tests := map[string]struct {
130
+ prepareMock func() *mockApcupsdConn
131
+ wantCollected map[string]int64
132
+ wantCharts int
133
+ wantConnDisconnect bool
134
+ }{
135
+ "case ok": {
136
+ prepareMock: prepareMockOk,
137
+ wantCollected: map[string]int64{
138
+ "battery_charge": 10000,
139
+ "battery_seconds_since_replacement": 86400,
140
+ "battery_voltage": 2790,
141
+ "battery_voltage_nominal": 2400,
142
+ "input_frequency": 5000,
143
+ "input_voltage": 23530,
144
+ "input_voltage_max": 23920,
145
+ "input_voltage_min": 23400,
146
+ "itemp": 3279,
147
+ "load": 55,
148
+ "load_percent": 930,
149
+ "output_voltage": 23660,
150
+ "output_voltage_nominal": 23000,
151
+ "selftest_BT": 0,
152
+ "selftest_IP": 0,
153
+ "selftest_NG": 0,
154
+ "selftest_NO": 1,
155
+ "selftest_OK": 0,
156
+ "selftest_UNK": 0,
157
+ "selftest_WN": 0,
158
+ "status_BOOST": 0,
159
+ "status_CAL": 0,
160
+ "status_COMMLOST": 0,
161
+ "status_LOWBATT": 0,
162
+ "status_NOBATT": 0,
163
+ "status_ONBATT": 0,
164
+ "status_ONLINE": 1,
165
+ "status_OVERLOAD": 0,
166
+ "status_REPLACEBATT": 0,
167
+ "status_SHUTTING_DOWN": 0,
168
+ "status_SLAVE": 0,
169
+ "status_SLAVEDOWN": 0,
170
+ "status_TRIM": 0,
171
+ "timeleft": 780000,
172
+ },
173
+ wantConnDisconnect: false,
174
+ },
175
+ "case commlost": {
176
+ prepareMock: prepareMockOkCommlost,
177
+ wantCollected: map[string]int64{
178
+ "status_BOOST": 0,
179
+ "status_CAL": 0,
180
+ "status_COMMLOST": 1,
181
+ "status_LOWBATT": 0,
182
+ "status_NOBATT": 0,
183
+ "status_ONBATT": 0,
184
+ "status_ONLINE": 0,
185
+ "status_OVERLOAD": 0,
186
+ "status_REPLACEBATT": 0,
187
+ "status_SHUTTING_DOWN": 0,
188
+ "status_SLAVE": 0,
189
+ "status_SLAVEDOWN": 0,
190
+ "status_TRIM": 0,
191
+ },
192
+ wantConnDisconnect: false,
193
+ },
194
+ "error on connect()": {
195
+ prepareMock: prepareMockErrOnConnect,
196
+ wantCollected: nil,
197
+ wantConnDisconnect: false,
198
+ },
199
+ "error on status()": {
200
+ prepareMock: prepareMockErrOnStatus,
201
+ wantCollected: nil,
202
+ wantConnDisconnect: true,
203
+ },
204
+ }
205
+
206
+ for name, test := range tests {
207
+ t.Run(name, func(t *testing.T) {
208
+ apc := New()
209
+ require.NoError(t, apc.Init())
210
+
211
+ mock := test.prepareMock()
212
+ apc.newConn = func(Config) apcupsdConn { return mock }
213
+
214
+ mx := apc.Collect()
215
+
216
+ if _, ok := mx["battery_seconds_since_replacement"]; ok {
217
+ mx["battery_seconds_since_replacement"] = 86400
218
+ }
219
+
220
+ assert.Equal(t, test.wantCollected, mx)
221
+
222
+ if len(test.wantCollected) > 0 {
223
+ if strings.Contains(name, "commlost") {
224
+ module.TestMetricsHasAllChartsDimsSkip(t, apc.Charts(), mx, func(chart *module.Chart) bool {
225
+ return chart.ID != statusChart.ID
226
+ })
227
+ } else {
228
+ module.TestMetricsHasAllChartsDims(t, apc.Charts(), mx)
229
+ }
230
+ }
231
+
232
+ assert.Equalf(t, test.wantConnDisconnect, mock.calledDisconnect, "calledDisconnect")
233
+ })
234
+ }
235
+}
236
+
237
+func prepareMockOk() *mockApcupsdConn {
238
+ return &mockApcupsdConn{
239
+ dataStatus: dataStatus,
240
+ }
241
+}
242
+
243
+func prepareMockOkCommlost() *mockApcupsdConn {
244
+ return &mockApcupsdConn{
245
+ dataStatus: dataStatusCommlost,
246
+ }
247
+}
248
+
249
+func prepareMockErrOnConnect() *mockApcupsdConn {
250
+ return &mockApcupsdConn{errOnConnect: true}
251
+}
252
+
253
+func prepareMockErrOnStatus() *mockApcupsdConn {
254
+ return &mockApcupsdConn{errOnStatus: true}
255
+}
256
+
257
+type mockApcupsdConn struct {
258
+ errOnConnect bool
259
+ errOnStatus bool
260
+ calledDisconnect bool
261
+
262
+ dataStatus []byte
263
+}
264
+
265
+func (m *mockApcupsdConn) connect() error {
266
+ if m.errOnConnect {
267
+ return errors.New("mock error on connect()")
268
+ }
269
+ return nil
270
+}
271
+
272
+func (m *mockApcupsdConn) disconnect() error {
273
+ m.calledDisconnect = true
274
+ return nil
275
+}
276
+
277
+func (m *mockApcupsdConn) status() ([]byte, error) {
278
+ if m.errOnStatus {
279
+ return nil, errors.New("mock error on status()")
280
+ }
281
+
282
+ return m.dataStatus, nil
283
+}
src/go/plugin/go.d/modules/apcupsd/charts.go
new
+224
@@ -0,0 +1,224 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package apcupsd
4
+
5
+import (
6
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
7
+)
8
+
9
+const (
10
+ prioUpsStatus = module.Priority + iota
11
+ prioUpsSelftest
12
+
13
+ prioUpsBatteryCharge
14
+ prioUpsBatteryTimeRemaining
15
+ prioUpsBatteryTimeSinceReplacement
16
+ prioUpsBatteryVoltage
17
+
18
+ prioUpsLoadCapacityUtilization
19
+ prioUpsLoad
20
+
21
+ prioUpsTemperature
22
+
23
+ prioUpsInputVoltage
24
+ prioUpsInputFrequency
25
+
26
+ prioUpsOutputVoltage
27
+)
28
+
29
+var charts = module.Charts{
30
+ statusChart.Copy(),
31
+ selftestChart.Copy(),
32
+
33
+ batteryChargeChart.Copy(),
34
+ batteryTimeRemainingChart.Copy(),
35
+ batteryTimeSinceReplacementChart.Copy(),
36
+ batteryVoltageChart.Copy(),
37
+
38
+ loadCapacityUtilizationChart.Copy(),
39
+ loadChart.Copy(),
40
+
41
+ internalTemperatureChart.Copy(),
42
+
43
+ inputVoltageChart.Copy(),
44
+ inputFrequencyChart.Copy(),
45
+
46
+ outputVoltageChart.Copy(),
47
+}
48
+
49
+// Status
50
+var (
51
+ statusChart = func() module.Chart {
52
+ chart := module.Chart{
53
+ ID: "ups_status",
54
+ Title: "UPS Status",
55
+ Units: "status",
56
+ Fam: "status",
57
+ Ctx: "apcupsd.ups_status",
58
+ Priority: prioUpsStatus,
59
+ Type: module.Line,
60
+ }
61
+ for _, v := range upsStatuses {
62
+ chart.Dims = append(chart.Dims, &module.Dim{ID: "status_" + v, Name: v})
63
+ }
64
+ return chart
65
+ }()
66
+ selftestChart = func() module.Chart {
67
+ chart := module.Chart{
68
+ ID: "ups_selftest",
69
+ Title: "UPS Self-Test Status",
70
+ Units: "status",
71
+ Fam: "status",
72
+ Ctx: "apcupsd.ups_selftest",
73
+ Priority: prioUpsSelftest,
74
+ Type: module.Line,
75
+ }
76
+ for _, v := range upsSelftestStatuses {
77
+ chart.Dims = append(chart.Dims, &module.Dim{ID: "selftest_" + v, Name: v})
78
+ }
79
+ return chart
80
+ }()
81
+)
82
+
83
+// Battery
84
+var (
85
+ batteryChargeChart = module.Chart{
86
+ ID: "ups_battery_charge",
87
+ Title: "UPS Battery Charge",
88
+ Units: "percent",
89
+ Fam: "battery",
90
+ Ctx: "apcupsd.ups_battery_charge",
91
+ Priority: prioUpsBatteryCharge,
92
+ Type: module.Area,
93
+ Dims: module.Dims{
94
+ {ID: "battery_charge", Name: "charge", Div: precision},
95
+ },
96
+ }
97
+ batteryTimeRemainingChart = module.Chart{
98
+ ID: "ups_battery_time_remaining",
99
+ Title: "UPS Estimated Runtime on Battery",
100
+ Units: "seconds",
101
+ Fam: "battery",
102
+ Ctx: "apcupsd.ups_battery_time_remaining",
103
+ Priority: prioUpsBatteryTimeRemaining,
104
+ Type: module.Line,
105
+ Dims: module.Dims{
106
+ {ID: "timeleft", Name: "timeleft", Div: precision},
107
+ },
108
+ }
109
+ batteryTimeSinceReplacementChart = module.Chart{
110
+ ID: "ups_battery_time_since_replacement",
111
+ Title: "UPS Time Since Battery Replacement",
112
+ Units: "seconds",
113
+ Fam: "battery",
114
+ Ctx: "apcupsd.ups_battery_time_since_replacement",
115
+ Priority: prioUpsBatteryTimeSinceReplacement,
116
+ Type: module.Line,
117
+ Dims: module.Dims{
118
+ {ID: "battery_seconds_since_replacement", Name: "since_replacement"},
119
+ },
120
+ }
121
+ batteryVoltageChart = module.Chart{
122
+ ID: "ups_battery_voltage",
123
+ Title: "UPS Battery Voltage",
124
+ Units: "Volts",
125
+ Fam: "battery",
126
+ Ctx: "apcupsd.ups_battery_voltage",
127
+ Priority: prioUpsBatteryVoltage,
128
+ Type: module.Line,
129
+ Dims: module.Dims{
130
+ {ID: "battery_voltage", Name: "voltage", Div: precision},
131
+ {ID: "battery_voltage_nominal", Name: "nominal_voltage", Div: precision},
132
+ },
133
+ }
134
+)
135
+
136
+// Load
137
+var (
138
+ loadCapacityUtilizationChart = module.Chart{
139
+ ID: "ups_load_capacity_utilization",
140
+ Title: "UPS Load Capacity Utilization",
141
+ Units: "percent",
142
+ Fam: "load",
143
+ Ctx: "apcupsd.ups_load_capacity_utilization",
144
+ Priority: prioUpsLoadCapacityUtilization,
145
+ Type: module.Line,
146
+ Dims: module.Dims{
147
+ {ID: "load_percent", Name: "load", Div: precision},
148
+ },
149
+ }
150
+ loadChart = module.Chart{
151
+ ID: "ups_load",
152
+ Title: "UPS Load",
153
+ Units: "Watts",
154
+ Fam: "load",
155
+ Ctx: "apcupsd.ups_load",
156
+ Priority: prioUpsLoad,
157
+ Type: module.Line,
158
+ Dims: module.Dims{
159
+ {ID: "load", Name: "load", Div: precision},
160
+ },
161
+ }
162
+)
163
+
164
+// Temperature
165
+var (
166
+ internalTemperatureChart = module.Chart{
167
+ ID: "ups_temperature",
168
+ Title: "UPS Internal Temperature",
169
+ Units: "Celsius",
170
+ Fam: "temperature",
171
+ Ctx: "apcupsd.ups_temperature",
172
+ Priority: prioUpsTemperature,
173
+ Type: module.Line,
174
+ Dims: module.Dims{
175
+ {ID: "itemp", Name: "temperature", Div: precision},
176
+ },
177
+ }
178
+)
179
+
180
+// Input
181
+var (
182
+ inputVoltageChart = module.Chart{
183
+ ID: "ups_input_voltage",
184
+ Title: "UPS Input Voltage",
185
+ Units: "Volts",
186
+ Fam: "input",
187
+ Ctx: "apcupsd.ups_input_voltage",
188
+ Priority: prioUpsInputVoltage,
189
+ Type: module.Line,
190
+ Dims: module.Dims{
191
+ {ID: "input_voltage", Name: "voltage", Div: precision},
192
+ {ID: "input_voltage_min", Name: "min_voltage", Div: precision},
193
+ {ID: "input_voltage_max", Name: "max_voltage", Div: precision},
194
+ },
195
+ }
196
+ inputFrequencyChart = module.Chart{
197
+ ID: "ups_input_frequency",
198
+ Title: "UPS Input Frequency",
199
+ Units: "Hz",
200
+ Fam: "input",
201
+ Ctx: "apcupsd.ups_input_frequency",
202
+ Priority: prioUpsInputFrequency,
203
+ Type: module.Line,
204
+ Dims: module.Dims{
205
+ {ID: "input_frequency", Name: "frequency", Div: precision},
206
+ },
207
+ }
208
+)
209
+
210
+// Output
211
+var (
212
+ outputVoltageChart = module.Chart{
213
+ ID: "ups_output_voltage",
214
+ Title: "UPS Output Voltage",
215
+ Units: "Volts",
216
+ Fam: "output",
217
+ Ctx: "apcupsd.ups_output_voltage",
218
+ Priority: prioUpsOutputVoltage,
219
+ Type: module.Line,
220
+ Dims: module.Dims{
221
+ {ID: "output_voltage", Name: "voltage", Div: precision},
222
+ },
223
+ }
224
+)
src/go/plugin/go.d/modules/apcupsd/client.go
new
+115
@@ -0,0 +1,115 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package apcupsd
4
+
5
+import (
6
+ "bytes"
7
+ "encoding/binary"
8
+ "io"
9
+ "net"
10
+ "time"
11
+)
12
+
13
+type apcupsdConn interface {
14
+ connect() error
15
+ disconnect() error
16
+ status() ([]byte, error)
17
+}
18
+
19
+func newUpsdConn(conf Config) apcupsdConn {
20
+ return &apcupsdClient{
21
+ address: conf.Address,
22
+ timeout: conf.Timeout.Duration(),
23
+ }
24
+}
25
+
26
+type apcupsdClient struct {
27
+ address string
28
+ timeout time.Duration
29
+ conn net.Conn
30
+}
31
+
32
+func (c *apcupsdClient) connect() error {
33
+ if c.conn != nil {
34
+ _ = c.disconnect()
35
+ }
36
+
37
+ conn, err := net.DialTimeout("tcp", c.address, c.timeout)
38
+ if err != nil {
39
+ return err
40
+ }
41
+
42
+ c.conn = conn
43
+
44
+ return nil
45
+}
46
+
47
+func (c *apcupsdClient) disconnect() error {
48
+ if c.conn != nil {
49
+ err := c.conn.Close()
50
+ c.conn = nil
51
+ return err
52
+ }
53
+ return nil
54
+}
55
+
56
+func (c *apcupsdClient) status() ([]byte, error) {
57
+ if err := c.send("status"); err != nil {
58
+ return nil, err
59
+ }
60
+ return c.receive()
61
+}
62
+
63
+func (c *apcupsdClient) send(cmd string) error {
64
+ // https://github.com/therealbstern/apcupsd/blob/224d19d5faa508d04267f6135fe53d50800550de/src/lib/apclibnis.c#L153
65
+
66
+ msgLength := make([]byte, 2)
67
+
68
+ binary.BigEndian.PutUint16(msgLength, uint16(len(cmd)))
69
+
70
+ if err := c.conn.SetWriteDeadline(c.deadline()); err != nil {
71
+ return err
72
+ }
73
+
74
+ if _, err := c.conn.Write(append(msgLength, cmd...)); err != nil {
75
+ return err
76
+ }
77
+
78
+ return nil
79
+}
80
+
81
+func (c *apcupsdClient) receive() ([]byte, error) {
82
+ // https://github.com/therealbstern/apcupsd/blob/224d19d5faa508d04267f6135fe53d50800550de/src/apcnis.c#L54
83
+
84
+ var buf bytes.Buffer
85
+ msgLength := make([]byte, 2)
86
+
87
+ for {
88
+ if err := c.conn.SetReadDeadline(c.deadline()); err != nil {
89
+ return nil, err
90
+ }
91
+
92
+ if _, err := io.ReadFull(c.conn, msgLength); err != nil {
93
+ return nil, err
94
+ }
95
+
96
+ length := binary.BigEndian.Uint16(msgLength)
97
+ if length == 0 {
98
+ break
99
+ }
100
+
101
+ if err := c.conn.SetReadDeadline(c.deadline()); err != nil {
102
+ return nil, err
103
+ }
104
+
105
+ if _, err := io.CopyN(&buf, c.conn, int64(length)); err != nil {
106
+ return nil, err
107
+ }
108
+ }
109
+
110
+ return buf.Bytes(), nil
111
+}
112
+
113
+func (c *apcupsdClient) deadline() time.Time {
114
+ return time.Now().Add(c.timeout)
115
+}
src/go/plugin/go.d/modules/apcupsd/collect.go
new
+144
@@ -0,0 +1,144 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package apcupsd
4
+
5
+import (
6
+ "errors"
7
+ "fmt"
8
+ "strings"
9
+ "time"
10
+)
11
+
12
+const precision = 100
13
+
14
+func (a *Apcupsd) collect() (map[string]int64, error) {
15
+ if a.conn == nil {
16
+ conn, err := a.establishConnection()
17
+ if err != nil {
18
+ return nil, err
19
+ }
20
+ a.conn = conn
21
+ }
22
+
23
+ resp, err := a.conn.status()
24
+ if err != nil {
25
+ a.Cleanup()
26
+ return nil, err
27
+ }
28
+
29
+ mx := make(map[string]int64)
30
+
31
+ if err := a.collectStatus(mx, resp); err != nil {
32
+ return nil, err
33
+ }
34
+
35
+ return mx, nil
36
+}
37
+
38
+func (a *Apcupsd) collectStatus(mx map[string]int64, resp []byte) error {
39
+ st, err := parseStatus(resp)
40
+ if err != nil {
41
+ return fmt.Errorf("failed to parse status: %v", err)
42
+ }
43
+
44
+ if st.status == "" {
45
+ return errors.New("unexpected response: status is empty")
46
+ }
47
+
48
+ for _, v := range upsStatuses {
49
+ mx["status_"+v] = 0
50
+ }
51
+ for _, v := range strings.Fields(st.status) {
52
+ mx["status_"+v] = 1
53
+ }
54
+
55
+ switch st.status {
56
+ case "COMMLOST", "SHUTTING_DOWN":
57
+ return nil
58
+ }
59
+
60
+ if st.selftest != "" {
61
+ for _, v := range upsSelftestStatuses {
62
+ mx["selftest_"+v] = 0
63
+ }
64
+ mx["selftest_"+st.selftest] = 1
65
+ }
66
+
67
+ if st.bcharge != nil {
68
+ mx["battery_charge"] = int64(*st.bcharge * precision)
69
+ }
70
+ if st.battv != nil {
71
+ mx["battery_voltage"] = int64(*st.battv * precision)
72
+ }
73
+ if st.nombattv != nil {
74
+ mx["battery_voltage_nominal"] = int64(*st.nombattv * precision)
75
+ }
76
+ if st.linev != nil {
77
+ mx["input_voltage"] = int64(*st.linev * precision)
78
+ }
79
+ if st.minlinev != nil {
80
+ mx["input_voltage_min"] = int64(*st.minlinev * precision)
81
+ }
82
+ if st.maxlinev != nil {
83
+ mx["input_voltage_max"] = int64(*st.maxlinev * precision)
84
+ }
85
+ if st.linefreq != nil {
86
+ mx["input_frequency"] = int64(*st.linefreq * precision)
87
+ }
88
+ if st.outputv != nil {
89
+ mx["output_voltage"] = int64(*st.outputv * precision)
90
+ }
91
+ if st.nomoutv != nil {
92
+ mx["output_voltage_nominal"] = int64(*st.nomoutv * precision)
93
+ }
94
+ if st.loadpct != nil {
95
+ mx["load_percent"] = int64(*st.loadpct * precision)
96
+ }
97
+ if st.itemp != nil {
98
+ mx["itemp"] = int64(*st.itemp * precision)
99
+ }
100
+ if st.timeleft != nil {
101
+ mx["timeleft"] = int64(*st.timeleft * 60 * precision) // to seconds
102
+ }
103
+ if st.nompower != nil && st.loadpct != nil {
104
+ mx["load"] = int64(*st.nompower * *st.loadpct / 100)
105
+ }
106
+ if st.battdate != "" {
107
+ if v, err := battdateSecondsAgo(st.battdate); err != nil {
108
+ a.Debugf("failed to calculate time since battery replacement for date '%s': %v", st.battdate, err)
109
+ } else {
110
+ mx["battery_seconds_since_replacement"] = v
111
+ }
112
+ }
113
+
114
+ return nil
115
+}
116
+
117
+func battdateSecondsAgo(battdate string) (int64, error) {
118
+ var layout string
119
+
120
+ if strings.ContainsRune(battdate, '-') {
121
+ layout = "2006-01-02"
122
+ } else {
123
+ layout = "01/02/06"
124
+ }
125
+
126
+ date, err := time.Parse(layout, battdate)
127
+ if err != nil {
128
+ return 0, err
129
+ }
130
+
131
+ secsAgo := int64(time.Now().Sub(date).Seconds())
132
+
133
+ return secsAgo, nil
134
+}
135
+
136
+func (a *Apcupsd) establishConnection() (apcupsdConn, error) {
137
+ conn := a.newConn(a.Config)
138
+
139
+ if err := conn.connect(); err != nil {
140
+ return nil, err
141
+ }
142
+
143
+ return conn, nil
144
+}
src/go/plugin/go.d/modules/apcupsd/config_schema.json
new
+44
@@ -0,0 +1,44 @@
1
+{
2
+ "jsonSchema": {
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "title": "Apcupsd collector configuration.",
5
+ "type": "object",
6
+ "properties": {
7
+ "update_every": {
8
+ "title": "Update every",
9
+ "description": "Data collection interval, measured in seconds.",
10
+ "type": "integer",
11
+ "minimum": 1,
12
+ "default": 1
13
+ },
14
+ "address": {
15
+ "title": "Address",
16
+ "description": "The IP address and port where the Apcupsd daemon listens for connections.",
17
+ "type": "string",
18
+ "default": "127.0.0.1:3551"
19
+ },
20
+ "timeout": {
21
+ "title": "Timeout",
22
+ "description": "Timeout for establishing a connection and communication (reading and writing) in seconds.",
23
+ "type": "number",
24
+ "minimum": 0.5,
25
+ "default": 1
26
+ }
27
+ },
28
+ "required": [
29
+ "address"
30
+ ],
31
+ "additionalProperties": false,
32
+ "patternProperties": {
33
+ "^name$": {}
34
+ }
35
+ },
36
+ "uiSchema": {
37
+ "uiOptions": {
38
+ "fullPage": true
39
+ },
40
+ "timeout": {
41
+ "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
42
+ }
43
+ }
44
+}
src/go/plugin/go.d/modules/apcupsd/metadata.yaml
new
+244
@@ -0,0 +1,244 @@
1
+plugin_name: go.d.plugin
2
+modules:
3
+ - meta:
4
+ id: collector-go.d.plugin-apcupsd
5
+ plugin_name: go.d.plugin
6
+ module_name: apcupsd
7
+ monitored_instance:
8
+ name: APC UPS
9
+ link: https://www.apc.com
10
+ icon_filename: apc.svg
11
+ categories:
12
+ - data-collection.ups
13
+ keywords:
14
+ - ups
15
+ - apcupsd
16
+ - apc
17
+ related_resources:
18
+ integrations:
19
+ list: []
20
+ info_provided_to_referring_integrations:
21
+ description: ""
22
+ most_popular: false
23
+ overview:
24
+ data_collection:
25
+ metrics_description: |
26
+ This collector monitors Uninterruptible Power Supplies by polling the Apcupsd daemon.
27
+ method_description: ""
28
+ supported_platforms:
29
+ include: []
30
+ exclude: []
31
+ multi_instance: true
32
+ additional_permissions:
33
+ description: ""
34
+ default_behavior:
35
+ auto_detection:
36
+ description: |
37
+ By default, it detects Apcupsd instances running on localhost that are listening on port 3551.
38
+ On startup, it tries to collect metrics from:
39
+
40
+ - 127.0.0.1:3551
41
+ limits:
42
+ description: ""
43
+ performance_impact:
44
+ description: ""
45
+ setup:
46
+ prerequisites:
47
+ list: []
48
+ configuration:
49
+ file:
50
+ name: go.d/apcupsd.conf
51
+ options:
52
+ description: |
53
+ The following options can be defined globally: update_every, autodetection_retry.
54
+ folding:
55
+ title: Config options
56
+ enabled: true
57
+ list:
58
+ - name: update_every
59
+ description: Data collection frequency.
60
+ default_value: 1
61
+ required: false
62
+ - name: autodetection_retry
63
+ description: Recheck interval in seconds. Zero means no recheck will be scheduled.
64
+ default_value: 0
65
+ required: false
66
+ - name: address
67
+ description: Apcupsd daemon address in IP:PORT format.
68
+ default_value: 127.0.0.1:3551
69
+ required: true
70
+ - name: timeout
71
+ description: Connection/read/write timeout in seconds. The timeout includes name resolution, if required.
72
+ default_value: 2
73
+ required: false
74
+ examples:
75
+ folding:
76
+ title: Config
77
+ enabled: true
78
+ list:
79
+ - name: Basic
80
+ description: A basic example configuration.
81
+ config: |
82
+ jobs:
83
+ - name: local
84
+ address: 127.0.0.1:3551
85
+ - name: Multi-instance
86
+ description: |
87
+ > **Note**: When you define multiple jobs, their names must be unique.
88
+
89
+ Collecting metrics from local and remote instances.
90
+ config: |
91
+ jobs:
92
+ - name: local
93
+ address: 127.0.0.1:3551
94
+
95
+ - name: remote
96
+ address: 203.0.113.0:3551
97
+ troubleshooting:
98
+ problems:
99
+ list: []
100
+ alerts:
101
+ - name: apcupsd_ups_load_capacity
102
+ metric: apcupsd.ups_load_capacity_utilization
103
+ info: "APC UPS average load over the last 10 minutes"
104
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/apcupsd.conf
105
+ - name: apcupsd_ups_battery_charge
106
+ metric: apcupsd.ups_battery_charge
107
+ info: "APC UPS average battery charge over the last minute"
108
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/apcupsd.conf
109
+ - name: apcupsd_last_collected_secs
110
+ metric: apcupsd.ups_status
111
+ info: "APC UPS number of seconds since the last successful data collection"
112
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/apcupsd.conf
113
+ - name: apcupsd_ups_selftest_warning
114
+ metric: apcupsd.ups_selftest
115
+ info: "APC UPS self-test failed due to insufficient battery capacity or due to overload"
116
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/apcupsd.conf
117
+ - name: apcupsd_ups_status_onbatt
118
+ metric: apcupsd.ups_status
119
+ info: "APC UPS has switched to battery power because the input power has failed"
120
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/apcupsd.conf
121
+ - name: apcupsd_ups_status_overload
122
+ metric: apcupsd.ups_status
123
+ info: "APC UPS is overloaded and cannot supply enough power to the load"
124
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/apcupsd.conf
125
+ - name: apcupsd_ups_status_lowbatt
126
+ metric: apcupsd.ups_status
127
+ info: "APC UPS battery is low and needs to be recharged"
128
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/apcupsd.conf
129
+ - name: apcupsd_ups_status_replacebatt
130
+ metric: apcupsd.ups_status
131
+ info: "APC UPS battery has reached the end of its lifespan and needs to be replaced"
132
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/apcupsd.conf
133
+ - name: apcupsd_ups_status_nobatt
134
+ metric: apcupsd.ups_status
135
+ info: "APC UPS has no battery"
136
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/apcupsd.conf
137
+ - name: apcupsd_ups_status_commlost
138
+ metric: apcupsd.ups_status
139
+ info: "APC UPS communication link is lost"
140
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/apcupsd.conf
141
+ metrics:
142
+ folding:
143
+ title: Metrics
144
+ enabled: false
145
+ description: ""
146
+ availability: []
147
+ scopes:
148
+ - name: ups
149
+ description: These metrics refer to the UPS unit.
150
+ labels: []
151
+ metrics:
152
+ - name: apcupsd.ups_status
153
+ description: UPS Status
154
+ unit: status
155
+ chart_type: line
156
+ dimensions:
157
+ - name: TRIM
158
+ - name: BOOST
159
+ - name: CAL
160
+ - name: ONLINE
161
+ - name: ONBATT
162
+ - name: OVERLOAD
163
+ - name: LOWBATT
164
+ - name: REPLACEBATT
165
+ - name: NOBATT
166
+ - name: SLAVE
167
+ - name: SLAVEDOWN
168
+ - name: COMMLOST
169
+ - name: SHUTTING_DOWN
170
+ - name: apcupsd.ups_selftest
171
+ description: UPS Self-Test Status
172
+ unit: status
173
+ chart_type: line
174
+ dimensions:
175
+ - name: NO
176
+ - name: NG
177
+ - name: WN
178
+ - name: IP
179
+ - name: OK
180
+ - name: BT
181
+ - name: UNK
182
+ - name: apcupsd.ups_battery_charge
183
+ description: UPS Battery Charge
184
+ unit: percent
185
+ chart_type: area
186
+ dimensions:
187
+ - name: charge
188
+ - name: apcupsd.ups_battery_time_remaining
189
+ description: UPS Estimated Runtime on Battery
190
+ unit: seconds
191
+ chart_type: line
192
+ dimensions:
193
+ - name: timeleft
194
+ - name: apcupsd.ups_battery_time_since_replacement
195
+ description: UPS Time Since Battery Replacement
196
+ unit: seconds
197
+ chart_type: line
198
+ dimensions:
199
+ - name: since_replacement
200
+ - name: apcupsd.ups_battery_voltage
201
+ description: UPS Battery Voltage
202
+ unit: Volts
203
+ chart_type: line
204
+ dimensions:
205
+ - name: voltage
206
+ - name: nominal_voltage
207
+ - name: apcupsd.ups_load_capacity_utilization
208
+ description: UPS Load Capacity Utilization
209
+ unit: percent
210
+ chart_type: area
211
+ dimensions:
212
+ - name: load
213
+ - name: apcupsd.ups_load
214
+ description: UPS Load
215
+ unit: Watts
216
+ chart_type: line
217
+ dimensions:
218
+ - name: load
219
+ - name: apcupsd.ups_temperature
220
+ description: UPS Internal Temperature
221
+ unit: Celsius
222
+ chart_type: line
223
+ dimensions:
224
+ - name: temperature
225
+ - name: apcupsd.ups_input_voltage
226
+ description: UPS Input Voltage
227
+ unit: Volts
228
+ chart_type: line
229
+ dimensions:
230
+ - name: voltage
231
+ - name: min_voltage
232
+ - name: max_voltage
233
+ - name: apcupsd.ups_input_frequency
234
+ description: UPS Input Frequency
235
+ unit: Hz
236
+ chart_type: line
237
+ dimensions:
238
+ - name: frequency
239
+ - name: apcupsd.ups_output_voltage
240
+ description: UPS Output Voltage
241
+ unit: Volts
242
+ chart_type: line
243
+ dimensions:
244
+ - name: voltage
src/go/plugin/go.d/modules/apcupsd/status.go
new
+137
@@ -0,0 +1,137 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package apcupsd
4
+
5
+import (
6
+ "bufio"
7
+ "bytes"
8
+ "fmt"
9
+ "strconv"
10
+ "strings"
11
+)
12
+
13
+var upsStatuses = []string{
14
+ "CAL",
15
+ "TRIM",
16
+ "BOOST",
17
+ "ONLINE",
18
+ "ONBATT",
19
+ "OVERLOAD",
20
+ "LOWBATT",
21
+ "REPLACEBATT",
22
+ "NOBATT",
23
+ "SLAVE",
24
+ "SLAVEDOWN",
25
+ "COMMLOST",
26
+ "SHUTTING_DOWN",
27
+}
28
+
29
+var upsSelftestStatuses = []string{
30
+ "NO",
31
+ "NG",
32
+ "WN",
33
+ "IP",
34
+ "OK",
35
+ "BT",
36
+ "UNK",
37
+}
38
+
39
+// examples: https://github.com/therealbstern/apcupsd/tree/master/examples/status
40
+type apcupsdStatus struct {
41
+ bcharge *float64 // battery charge level (percentage)
42
+ battv *float64 // battery voltage (Volts)
43
+ nombattv *float64 // nominal battery voltage (Volts)
44
+ linev *float64 // line voltage (Volts)
45
+ minlinev *float64 // min line voltage (Volts)
46
+ maxlinev *float64 // max line voltage (Volts)
47
+ linefreq *float64 // line frequency (Hz)
48
+ outputv *float64 // output voltage (Volts)
49
+ nomoutv *float64 // nominal output voltage (Volts)
50
+ loadpct *float64 // UPS Load (Percent Load Capacity)
51
+ itemp *float64 // internal UPS temperature (Celsius)
52
+ nompower *float64 // nominal power (Watts)
53
+ timeleft *float64 // estimated runtime left (minutes)
54
+ battdate string // Last battery change date (MM/DD/YY or YYYY-MM-DD)
55
+ status string
56
+ selftest string
57
+}
58
+
59
+func parseStatus(resp []byte) (*apcupsdStatus, error) {
60
+ var st apcupsdStatus
61
+ sc := bufio.NewScanner(bytes.NewBuffer(resp))
62
+
63
+ for sc.Scan() {
64
+ line := sc.Text()
65
+
66
+ key, value, ok := strings.Cut(line, ":")
67
+ if !ok {
68
+ continue
69
+ }
70
+
71
+ key, value = strings.TrimSpace(key), strings.TrimSpace(value)
72
+
73
+ if value == "N/A" {
74
+ continue
75
+ }
76
+
77
+ var err error
78
+
79
+ // https://github.com/therealbstern/apcupsd/blob/224d19d5faa508d04267f6135fe53d50800550de/src/lib/apcstatus.c#L30
80
+ switch key {
81
+ case "BCHARGE":
82
+ st.bcharge, err = parseFloat(value)
83
+ case "BATTV":
84
+ st.battv, err = parseFloat(value)
85
+ case "NOMBATTV":
86
+ st.nombattv, err = parseFloat(value)
87
+ case "LINEV":
88
+ st.linev, err = parseFloat(value)
89
+ case "MINLINEV":
90
+ st.minlinev, err = parseFloat(value)
91
+ case "MAXLINEV":
92
+ st.maxlinev, err = parseFloat(value)
93
+ case "LINEFREQ":
94
+ st.linefreq, err = parseFloat(value)
95
+ case "OUTPUTV":
96
+ st.outputv, err = parseFloat(value)
97
+ case "NOMOUTV":
98
+ st.nomoutv, err = parseFloat(value)
99
+ case "LOADPCT":
100
+ st.loadpct, err = parseFloat(value)
101
+ case "ITEMP":
102
+ st.itemp, err = parseFloat(value)
103
+ case "NOMPOWER":
104
+ st.nompower, err = parseFloat(value)
105
+ case "TIMELEFT":
106
+ st.timeleft, err = parseFloat(value)
107
+ case "BATTDATE":
108
+ st.battdate = value
109
+ case "STATUS":
110
+ if value == "SHUTTING DOWN" {
111
+ value = "SHUTTING_DOWN"
112
+ }
113
+ st.status = value
114
+ case "SELFTEST":
115
+ if value == "??" {
116
+ value = "UNK"
117
+ }
118
+ st.selftest = value
119
+ default:
120
+ continue
121
+ }
122
+ if err != nil {
123
+ return nil, fmt.Errorf("line '%s': %v", line, err)
124
+ }
125
+ }
126
+
127
+ return &st, nil
128
+}
129
+
130
+func parseFloat(s string) (*float64, error) {
131
+ val, _, _ := strings.Cut(s, " ")
132
+ f, err := strconv.ParseFloat(val, 64)
133
+ if err != nil {
134
+ return nil, err
135
+ }
136
+ return &f, nil
137
+}
src/go/plugin/go.d/modules/apcupsd/testdata/config.json
new
+5
@@ -0,0 +1,5 @@
1
+{
2
+ "update_every": 123,
3
+ "address": "ok",
4
+ "timeout": 123.123
5
+}
src/go/plugin/go.d/modules/apcupsd/testdata/config.yaml
new
+3
@@ -0,0 +1,3 @@
1
+update_every: 123
2
+address: "ok"
3
+timeout: 123.123
src/go/plugin/go.d/modules/apcupsd/testdata/status.txt
new
+56
@@ -0,0 +1,56 @@
1
+DATE : Wed Sep 27 17:30:23 CEST 2000
2
+HOSTNAME : test
3
+RELEASE : 3.7.3-20000925
4
+CABLE : Custom Cable Smart
5
+MODEL : SMART-UPS 1000
6
+UPSMODE : Stand Alone
7
+STARTTIME: Wed Sep 27 10:39:23 CEST 2000
8
+UPSNAME : UPS_IDEN
9
+STATUS : ONLINE
10
+LINEV : 235.3 Volts
11
+LOADPCT : 9.3 Percent Load Capacity
12
+BCHARGE : 100.0 Percent
13
+TIMELEFT : 130.0 Minutes
14
+MBATTCHG : 5 Percent
15
+MINTIMEL : 3 Minutes
16
+MAXTIME : 0 Seconds
17
+MAXLINEV : 239.2 Volts
18
+MINLINEV : 234.0 Volts
19
+OUTPUTV : 236.6 Volts
20
+SENSE : High
21
+DWAKE : 000 Seconds
22
+DSHUTD : 020 Seconds
23
+DLOWBATT : 02 Minutes
24
+LOTRANS : 196.0 Volts
25
+HITRANS : 253.0 Volts
26
+RETPCT : 000.0 Percent
27
+ITEMP : 32.8 C Internal
28
+ALARMDEL : 5 seconds
29
+BATTV : 27.9 Volts
30
+LINEFREQ : 50.0 Hz
31
+LASTXFER : Line voltage notch or spike
32
+NUMXFERS : 0
33
+XONBATT : N/A
34
+TONBATT : 0 seconds
35
+CUMONBATT: 0 seconds
36
+XOFFBATT : N/A
37
+SELFTEST : NO
38
+STESTI : 336
39
+STATFLAG : 0x08 Status Flag
40
+DIPSW : 0x00 Dip Switch
41
+REG1 : 0x00 Register 1
42
+REG2 : 0x00 Register 2
43
+REG3 : 0x00 Register 3
44
+MANDATE : 07/31/99
45
+SERIALNO : QS9931125245
46
+BATTDATE : 07/31/99
47
+NOMOUTV : 230
48
+NOMBATTV : 24.0
49
+NOMPOWER : 600 Watts
50
+HUMIDITY : N/A
51
+AMBTEMP : N/A
52
+EXTBATTS : 0
53
+BADBATTS : N/A
54
+FIRMWARE : 60.11.I
55
+APCMODEL : IWI
56
+END APC : Wed Sep 27 17:30:31 CEST 2000
src/go/plugin/go.d/modules/apcupsd/testdata/status_commlost.txt
new
+18
@@ -0,0 +1,18 @@
1
+APC : 001,017,0427
2
+DATE : 2024-09-06 20:22:06 +0300
3
+HOSTNAME : test
4
+VERSION : 3.14.14 (31 May 2016) debian
5
+CABLE : USB Cable
6
+DRIVER : USB UPS Driver
7
+UPSMODE : Stand Alone
8
+STARTTIME: 2024-09-03 10:15:36 +0300
9
+STATUS : COMMLOST
10
+MBATTCHG : 5 Percent
11
+MINTIMEL : 3 Minutes
12
+MAXTIME : 0 Seconds
13
+NUMXFERS : 0
14
+TONBATT : 0 Seconds
15
+CUMONBATT: 0 Seconds
16
+XOFFBATT : N/A
17
+STATFLAG : 0x05000100
18
+END APC : 2024-09-07 19:26:34 +0300
src/go/plugin/go.d/modules/init.go
+1
@@ -7,6 +7,7 @@ import (
7
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/adaptecraid"
8
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/ap"
9
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/apache"
10
+ _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/apcupsd"
11
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/beanstalk"
12
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/bind"
13
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/boinc"
src/health/health.d/apcupsd.conf
+23
-25
@@ -1,11 +1,11 @@
1
# you can disable an alarm notification by setting the 'to' line to: silent
2
3
- template: apcupsd_10min_ups_load
4
- on: apcupsd.load
3
+ template: apcupsd_ups_load_capacity
4
+ on: apcupsd.ups_load_capacity_utilization
5
class: Utilization
6
type: Power Supply
7
-component: UPS
8
- lookup: average -10m unaligned of percentage
7
+component: UPS device
8
+ lookup: average -10m unaligned of load
9
units: %
10
every: 1m
11
warn: $this > (($status >= $WARNING) ? (70) : (80))
@@ -14,13 +14,11 @@ component: UPS
14
info: APC UPS average load over the last 10 minutes
15
to: sitemgr
16
17
-# Discussion in https://github.com/netdata/netdata/pull/3928:
18
-# Fire the alarm as soon as it's going on battery (99% charge) and clear only when full.
19
- template: apcupsd_ups_charge
20
- on: apcupsd.charge
17
+ template: apcupsd_ups_battery_charge
18
+ on: apcupsd.ups_battery_charge
19
class: Errors
20
type: Power Supply
23
-component: UPS
21
+component: UPS device
22
lookup: average -60s unaligned of charge
23
units: %
24
every: 60s
@@ -32,7 +30,7 @@ component: UPS
30
to: sitemgr
31
32
template: apcupsd_last_collected_secs
35
- on: apcupsd.load
33
+ on: apcupsd.ups_status
34
class: Latency
35
type: Power Supply
36
component: UPS device
@@ -47,21 +45,21 @@ component: UPS device
45
46
#Send out a warning when SELFTEST code is BT or NG. Code descriptions can be found at:
47
#http://www.apcupsd.org/manual/#:~:text=or%20N/A.-,SELFTEST,-The%20results%20of
50
- template: apcupsd_selftest_warning
51
- on: apcupsd.selftest
48
+ template: apcupsd_ups_selftest_warning
49
+ on: apcupsd.ups_selftest
50
lookup: max -1s unaligned match-names of BT,NG
51
units: status
52
every: 10s
53
warn: $this == 1
54
delay: up 0 down 15m multiplier 1.5 max 1h
57
- info: APC UPS self-test failed due to insufficient battery capacity or due to overload.
55
+ info: APC UPS self-test failed due to insufficient battery capacity or due to overload
56
to: sitemgr
57
58
#Send out a warning when STATUS code is ONBATT,OVERLOAD,LOWBATT,REPLACEBATT,NOBATT,COMMLOST
59
#https://man.archlinux.org/man/apcaccess.8.en#:~:text=apcupsd%20was%20started-,STATUS,-%3A%20UPS%20status.%20One
60
63
- template: apcupsd_status_onbatt
64
- on: apcupsd.status
61
+ template: apcupsd_ups_status_onbatt
62
+ on: apcupsd.ups_status
63
lookup: max -1s unaligned match-names of ONBATT
64
units: status
65
every: 10s
@@ -70,8 +68,8 @@ component: UPS device
68
info: APC UPS has switched to battery power because the input power has failed
69
to: sitemgr
70
73
- template: apcupsd_status_overload
74
- on: apcupsd.status
71
+ template: apcupsd_ups_status_overload
72
+ on: apcupsd.ups_status
73
lookup: max -1s unaligned match-names of OVERLOAD
74
units: status
75
every: 10s
@@ -80,8 +78,8 @@ component: UPS device
78
info: APC UPS is overloaded and cannot supply enough power to the load
79
to: sitemgr
80
83
- template: apcupsd_status_lowbatt
84
- on: apcupsd.status
81
+ template: apcupsd_ups_status_lowbatt
82
+ on: apcupsd.ups_status
83
lookup: max -1s unaligned match-names of LOWBATT
84
units: status
85
every: 10s
@@ -90,8 +88,8 @@ component: UPS device
88
info: APC UPS battery is low and needs to be recharged
89
to: sitemgr
90
93
- template: apcupsd_status_replacebatt
94
- on: apcupsd.status
91
+ template: apcupsd_ups_status_replacebatt
92
+ on: apcupsd.ups_status
93
lookup: max -1s unaligned match-names of REPLACEBATT
94
units: status
95
every: 10s
@@ -100,8 +98,8 @@ component: UPS device
98
info: APC UPS battery has reached the end of its lifespan and needs to be replaced
99
to: sitemgr
100
103
- template: apcupsd_status_nobatt
104
- on: apcupsd.status
101
+ template: apcupsd_ups_status_nobatt
102
+ on: apcupsd.ups_status
103
lookup: max -1s unaligned match-names of NOBATT
104
units: status
105
every: 10s
@@ -110,8 +108,8 @@ component: UPS device
108
info: APC UPS has no battery
109
to: sitemgr
110
113
- template: apcupsd_status_commlost
114
- on: apcupsd.status
111
+ template: apcupsd_ups_status_commlost
112
+ on: apcupsd.ups_status
113
lookup: max -1s unaligned match-names of COMMLOST
114
units: status
115
every: 10s