394 lines
8.6 KiB
Go
394 lines
8.6 KiB
Go
|
package modbus
|
||
|
|
||
|
import (
|
||
|
"testing"
|
||
|
"time"
|
||
|
|
||
|
mb "github.com/grid-x/modbus"
|
||
|
"github.com/stretchr/testify/require"
|
||
|
"github.com/tbrandon/mbserver"
|
||
|
|
||
|
"github.com/influxdata/telegraf"
|
||
|
"github.com/influxdata/telegraf/metric"
|
||
|
"github.com/influxdata/telegraf/testutil"
|
||
|
)
|
||
|
|
||
|
func TestMetric(t *testing.T) {
|
||
|
plugin := Modbus{
|
||
|
Name: "Test",
|
||
|
Controller: "tcp://localhost:1502",
|
||
|
ConfigurationType: "metric",
|
||
|
Log: testutil.Logger{},
|
||
|
}
|
||
|
plugin.Metrics = []metricDefinition{
|
||
|
{
|
||
|
SlaveID: 1,
|
||
|
ByteOrder: "ABCD",
|
||
|
Measurement: "test",
|
||
|
Fields: []metricFieldDefinition{
|
||
|
{
|
||
|
Name: "coil-0",
|
||
|
Address: uint16(0),
|
||
|
RegisterType: "coil",
|
||
|
},
|
||
|
{
|
||
|
Name: "coil-1",
|
||
|
Address: uint16(1),
|
||
|
RegisterType: "coil",
|
||
|
},
|
||
|
{
|
||
|
Name: "holding-0",
|
||
|
Address: uint16(0),
|
||
|
InputType: "INT16",
|
||
|
},
|
||
|
{
|
||
|
Name: "holding-1",
|
||
|
Address: uint16(1),
|
||
|
InputType: "UINT16",
|
||
|
RegisterType: "holding",
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
SlaveID: 1,
|
||
|
ByteOrder: "ABCD",
|
||
|
Fields: []metricFieldDefinition{
|
||
|
{
|
||
|
Name: "coil-0",
|
||
|
Address: uint16(2),
|
||
|
RegisterType: "coil",
|
||
|
},
|
||
|
{
|
||
|
Name: "coil-1",
|
||
|
Address: uint16(3),
|
||
|
RegisterType: "coil",
|
||
|
},
|
||
|
{
|
||
|
Name: "holding-0",
|
||
|
Address: uint16(2),
|
||
|
InputType: "INT64",
|
||
|
Scale: 1.2,
|
||
|
OutputType: "FLOAT64",
|
||
|
},
|
||
|
},
|
||
|
Tags: map[string]string{
|
||
|
"location": "main building",
|
||
|
"device": "mydevice",
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
SlaveID: 2,
|
||
|
Fields: []metricFieldDefinition{
|
||
|
{
|
||
|
Name: "coil-6",
|
||
|
Address: uint16(6),
|
||
|
RegisterType: "coil",
|
||
|
},
|
||
|
{
|
||
|
Name: "coil-7",
|
||
|
Address: uint16(7),
|
||
|
RegisterType: "coil",
|
||
|
},
|
||
|
{
|
||
|
Name: "discrete-0",
|
||
|
Address: uint16(0),
|
||
|
RegisterType: "discrete",
|
||
|
},
|
||
|
{
|
||
|
Name: "holding-99",
|
||
|
Address: uint16(99),
|
||
|
InputType: "INT16",
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
SlaveID: 2,
|
||
|
Fields: []metricFieldDefinition{
|
||
|
{
|
||
|
Name: "coil-4",
|
||
|
Address: uint16(4),
|
||
|
RegisterType: "coil",
|
||
|
},
|
||
|
{
|
||
|
Name: "coil-5",
|
||
|
Address: uint16(5),
|
||
|
RegisterType: "coil",
|
||
|
},
|
||
|
{
|
||
|
Name: "input-0",
|
||
|
Address: uint16(0),
|
||
|
RegisterType: "input",
|
||
|
InputType: "UINT16",
|
||
|
},
|
||
|
{
|
||
|
Name: "input-1",
|
||
|
Address: uint16(2),
|
||
|
RegisterType: "input",
|
||
|
InputType: "UINT16",
|
||
|
},
|
||
|
{
|
||
|
Name: "holding-9",
|
||
|
Address: uint16(9),
|
||
|
InputType: "INT16",
|
||
|
},
|
||
|
},
|
||
|
Tags: map[string]string{
|
||
|
"location": "main building",
|
||
|
"device": "mydevice",
|
||
|
},
|
||
|
},
|
||
|
}
|
||
|
|
||
|
require.NoError(t, plugin.Init())
|
||
|
require.NotEmpty(t, plugin.requests)
|
||
|
|
||
|
require.NotNil(t, plugin.requests[1])
|
||
|
require.Len(t, plugin.requests[1].coil, 1, "coil 1")
|
||
|
require.Len(t, plugin.requests[1].holding, 1, "holding 1")
|
||
|
require.Empty(t, plugin.requests[1].discrete)
|
||
|
require.Empty(t, plugin.requests[1].input)
|
||
|
|
||
|
require.NotNil(t, plugin.requests[2])
|
||
|
require.Len(t, plugin.requests[2].coil, 1, "coil 2")
|
||
|
require.Len(t, plugin.requests[2].holding, 2, "holding 2")
|
||
|
require.Len(t, plugin.requests[2].discrete, 1, "discrete 2")
|
||
|
require.Len(t, plugin.requests[2].input, 2, "input 2")
|
||
|
}
|
||
|
|
||
|
func TestMetricResult(t *testing.T) {
|
||
|
data := []byte{
|
||
|
0x00, 0x0A, // 10
|
||
|
0x00, 0x2A, // 42
|
||
|
0x00, 0x00, 0x08, 0x98, // 2200
|
||
|
0x00, 0x00, 0x08, 0x99, // 2201
|
||
|
0x00, 0x00, 0x08, 0x9A, // 2202
|
||
|
0x40, 0x49, 0x0f, 0xdb, // float32 of 3.1415927410125732421875
|
||
|
0x4d, 0x6f, 0x64, 0x62, 0x75, 0x73, 0x20, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x00, // String "Modbus String"
|
||
|
}
|
||
|
|
||
|
// Write the data to a fake server
|
||
|
serv := mbserver.NewServer()
|
||
|
require.NoError(t, serv.ListenTCP("localhost:1502"))
|
||
|
defer serv.Close()
|
||
|
|
||
|
handler := mb.NewTCPClientHandler("localhost:1502")
|
||
|
require.NoError(t, handler.Connect())
|
||
|
defer handler.Close()
|
||
|
client := mb.NewClient(handler)
|
||
|
|
||
|
quantity := uint16(len(data) / 2)
|
||
|
_, err := client.WriteMultipleRegisters(1, quantity, data)
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
// Setup the plugin
|
||
|
plugin := Modbus{
|
||
|
Name: "FAKEMETER",
|
||
|
Controller: "tcp://localhost:1502",
|
||
|
ConfigurationType: "metric",
|
||
|
Log: testutil.Logger{},
|
||
|
}
|
||
|
plugin.Metrics = []metricDefinition{
|
||
|
{
|
||
|
SlaveID: 1,
|
||
|
ByteOrder: "ABCD",
|
||
|
Measurement: "machine",
|
||
|
Fields: []metricFieldDefinition{
|
||
|
{
|
||
|
Name: "hours",
|
||
|
Address: uint16(1),
|
||
|
InputType: "UINT16",
|
||
|
RegisterType: "holding",
|
||
|
},
|
||
|
{
|
||
|
Name: "temperature",
|
||
|
Address: uint16(2),
|
||
|
InputType: "INT16",
|
||
|
RegisterType: "holding",
|
||
|
},
|
||
|
{
|
||
|
Name: "comment",
|
||
|
Address: uint16(11),
|
||
|
Length: 7,
|
||
|
InputType: "STRING",
|
||
|
RegisterType: "holding",
|
||
|
},
|
||
|
},
|
||
|
Tags: map[string]string{
|
||
|
"location": "main building",
|
||
|
"device": "machine A",
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
SlaveID: 1,
|
||
|
ByteOrder: "ABCD",
|
||
|
Measurement: "machine",
|
||
|
Fields: []metricFieldDefinition{
|
||
|
{
|
||
|
Name: "hours",
|
||
|
Address: uint16(3),
|
||
|
InputType: "UINT32",
|
||
|
Scale: 0.01,
|
||
|
},
|
||
|
{
|
||
|
Name: "temperature",
|
||
|
Address: uint16(5),
|
||
|
InputType: "INT32",
|
||
|
Scale: 0.02,
|
||
|
},
|
||
|
{
|
||
|
Name: "output",
|
||
|
Address: uint16(7),
|
||
|
InputType: "UINT32",
|
||
|
},
|
||
|
},
|
||
|
Tags: map[string]string{
|
||
|
"location": "main building",
|
||
|
"device": "machine B",
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
SlaveID: 1,
|
||
|
Fields: []metricFieldDefinition{
|
||
|
{
|
||
|
Name: "pi",
|
||
|
Address: uint16(9),
|
||
|
InputType: "FLOAT32",
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
SlaveID: 1,
|
||
|
Measurement: "bitvalues",
|
||
|
Fields: []metricFieldDefinition{
|
||
|
{
|
||
|
Name: "bit 0",
|
||
|
Address: uint16(1),
|
||
|
InputType: "BIT",
|
||
|
Bit: 0,
|
||
|
},
|
||
|
{
|
||
|
Name: "bit 1",
|
||
|
Address: uint16(1),
|
||
|
InputType: "BIT",
|
||
|
Bit: 1,
|
||
|
},
|
||
|
{
|
||
|
Name: "bit 2",
|
||
|
Address: uint16(1),
|
||
|
InputType: "BIT",
|
||
|
Bit: 2,
|
||
|
},
|
||
|
{
|
||
|
Name: "bit 3",
|
||
|
Address: uint16(1),
|
||
|
InputType: "BIT",
|
||
|
Bit: 3,
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
}
|
||
|
require.NoError(t, plugin.Init())
|
||
|
|
||
|
// Check the generated requests
|
||
|
require.Len(t, plugin.requests, 1)
|
||
|
require.NotNil(t, plugin.requests[1])
|
||
|
require.Len(t, plugin.requests[1].holding, 5)
|
||
|
require.Empty(t, plugin.requests[1].coil)
|
||
|
require.Empty(t, plugin.requests[1].discrete)
|
||
|
require.Empty(t, plugin.requests[1].input)
|
||
|
|
||
|
// Gather the data and verify the resulting metrics
|
||
|
var acc testutil.Accumulator
|
||
|
require.NoError(t, plugin.Gather(&acc))
|
||
|
|
||
|
expected := []telegraf.Metric{
|
||
|
metric.New(
|
||
|
"machine",
|
||
|
map[string]string{
|
||
|
"name": "FAKEMETER",
|
||
|
"location": "main building",
|
||
|
"device": "machine A",
|
||
|
"slave_id": "1",
|
||
|
"type": "holding_register",
|
||
|
},
|
||
|
map[string]interface{}{
|
||
|
"hours": uint64(10),
|
||
|
"temperature": int64(42),
|
||
|
"comment": "Modbus String",
|
||
|
},
|
||
|
time.Unix(0, 0),
|
||
|
),
|
||
|
metric.New(
|
||
|
"machine",
|
||
|
map[string]string{
|
||
|
"name": "FAKEMETER",
|
||
|
"location": "main building",
|
||
|
"device": "machine B",
|
||
|
"slave_id": "1",
|
||
|
"type": "holding_register",
|
||
|
},
|
||
|
map[string]interface{}{
|
||
|
"hours": float64(22.0),
|
||
|
"temperature": float64(44.02),
|
||
|
"output": uint64(2202),
|
||
|
},
|
||
|
time.Unix(0, 0),
|
||
|
),
|
||
|
metric.New(
|
||
|
"modbus",
|
||
|
map[string]string{
|
||
|
"name": "FAKEMETER",
|
||
|
"slave_id": "1",
|
||
|
"type": "holding_register",
|
||
|
},
|
||
|
map[string]interface{}{"pi": float64(3.1415927410125732421875)},
|
||
|
time.Unix(0, 0),
|
||
|
),
|
||
|
metric.New(
|
||
|
"bitvalues",
|
||
|
map[string]string{
|
||
|
"name": "FAKEMETER",
|
||
|
"slave_id": "1",
|
||
|
"type": "holding_register",
|
||
|
},
|
||
|
map[string]interface{}{
|
||
|
"bit 0": uint64(0),
|
||
|
"bit 1": uint64(1),
|
||
|
"bit 2": uint64(0),
|
||
|
"bit 3": uint64(1),
|
||
|
},
|
||
|
time.Unix(0, 0),
|
||
|
),
|
||
|
}
|
||
|
|
||
|
actual := acc.GetTelegrafMetrics()
|
||
|
testutil.RequireMetricsEqual(t, expected, actual, testutil.IgnoreTime(), testutil.SortMetrics())
|
||
|
}
|
||
|
|
||
|
func TestMetricAddressOverflow(t *testing.T) {
|
||
|
logger := &testutil.CaptureLogger{}
|
||
|
plugin := Modbus{
|
||
|
Name: "Test",
|
||
|
Controller: "tcp://localhost:1502",
|
||
|
ConfigurationType: "metric",
|
||
|
Log: logger,
|
||
|
Workarounds: workarounds{ReadCoilsStartingAtZero: true},
|
||
|
}
|
||
|
plugin.Metrics = []metricDefinition{
|
||
|
{
|
||
|
SlaveID: 1,
|
||
|
ByteOrder: "ABCD",
|
||
|
Measurement: "test",
|
||
|
Fields: []metricFieldDefinition{
|
||
|
{
|
||
|
Name: "field",
|
||
|
Address: uint16(65534),
|
||
|
InputType: "UINT64",
|
||
|
RegisterType: "holding",
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
}
|
||
|
require.ErrorIs(t, plugin.Init(), errAddressOverflow)
|
||
|
}
|