feat(inputs.modbus): Add per-metric configuration style (#13507)

This commit is contained in:
Sven Rebhan 2023-06-30 20:47:16 +02:00 committed by GitHub
parent 657eca5cf0
commit 96b9845853
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 5450 additions and 4448 deletions

View File

@ -1,3 +1,4 @@
<!-- markdownlint-disable MD024 -->
# Modbus Input Plugin
The Modbus plugin collects Discrete Inputs, Coils, Input Registers and Holding
@ -14,7 +15,7 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
## Configuration
```toml @sample_general_begin.conf @sample_register.conf @sample_request.conf @sample_general_end.conf
```toml @sample_general_begin.conf @sample_register.conf @sample_request.conf @sample_metric.conf @sample_general_end.conf
# Retrieve data from MODBUS slave devices
[[inputs.modbus]]
## Connection Configuration
@ -66,6 +67,7 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
## Define the configuration schema
## |---register -- define fields per register type in the original style (only supports one slave ID)
## |---request -- define fields on a requests base
## |---metric -- define fields on a metric base
configuration_type = "register"
## --- "register" configuration style ---
@ -238,6 +240,74 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
machine = "impresser"
location = "main building"
## --- "metric" configuration style ---
## Per metric definition
##
## Request optimization algorithm across metrics
## |---none -- Do not perform any optimization and just group requests
## | within metrics (default)
## |---max_insert -- Collate registers across all defined metrics and fill in
## holes to optimize the number of requests.
# optimization = "none"
## Maximum number of registers the optimizer is allowed to insert between
## non-consecutive registers to save requests.
## This option is only used for the 'max_insert' optimization strategy and
## effectively denotes the hole size between registers to fill.
# optimization_max_register_fill = 50
## Define a metric produced by the requests to the device
## Multiple of those metrics can be defined. The referenced registers will
## be collated into requests send to the device
[[inputs.modbus.metric]]
## ID of the modbus slave device to query
## If you need to query multiple slave-devices, create several "metric" definitions.
slave_id = 1
## Byte order of the data
## |---ABCD -- Big Endian (Motorola)
## |---DCBA -- Little Endian (Intel)
## |---BADC -- Big Endian with byte swap
## |---CDAB -- Little Endian with byte swap
# byte_order = "ABCD"
## Name of the measurement
# measurement = "modbus"
## Field definitions
## register - type of the modbus register, can be "coil", "discrete",
## "holding" or "input". Defaults to "holding".
## address - address of the register to query. For coil and discrete inputs this is the bit address.
## name - field name
## type *1 - type of the modbus field, can be
## INT8L, INT8H, UINT8L, UINT8H (low and high byte variants)
## INT16, UINT16, INT32, UINT32, INT64, UINT64 and
## FLOAT16, FLOAT32, FLOAT64 (IEEE 754 binary representation)
## scale *1 - (optional) factor to scale the variable with
## output *2 - (optional) type of resulting field, can be INT64, UINT64 or FLOAT64. Defaults to FLOAT64 if
## "scale" is provided and to the input "type" class otherwise (i.e. INT* -> INT64, etc).
##
## *1: These fields are ignored for both "coil" and "discrete"-input type of registers.
## *2: This field can only be "UINT16" or "BOOL" if specified for both "coil"
## and "discrete"-input type of registers. By default the fields are
## output as zero or one in UINT16 format unless "BOOL" is used.
fields = [
{ register="coil", address=0, name="door_open"},
{ register="coil", address=1, name="status_ok"},
{ register="holding", address=0, name="voltage", type="INT16" },
{ address=1, name="current", type="INT32", scale=0.001 },
{ address=5, name="energy", type="FLOAT32", scale=0.001,},
{ address=7, name="frequency", type="UINT32", scale=0.1 },
{ address=8, name="power_factor", type="INT64", scale=0.01 },
]
## Tags assigned to the metric
# [inputs.modbus.metric.tags]
# machine = "impresser"
# location = "main building"
## RS485 specific settings. Only take effect for serial controllers.
## Note: This has to be at the end of the modbus configuration due to
## TOML constraints.
@ -304,6 +374,7 @@ Directly jump to the styles:
- [original / register plugin style](#register-configuration-style)
- [per-request style](#request-configuration-style)
- [per-metrict style](#metric-configuration-style)
---
@ -554,6 +625,143 @@ __Please note:__ These tags take precedence over predefined tags such as `name`,
---
### `metric` configuration style
This style can be used to specify the desired metrics directly instead of
focusing on the modbus view. Multiple `[[inputs.modbus.metric]]` sections
including multiple slave-devices can be specified. This way, _modbus_ gateway
devices can be queried. The plugin automatically collects registers across
the specified metrics, groups them per slave and register-type and (optionally)
optimizes the resulting requests for non-consecutive addresses.
#### Slave device
You can use the `slave_id` setting to specify the ID of the slave device to
query. It should be specified for each metric section, otherwise it defaults to
zero. Please note, only one `slave_id` can be specified per metric section.
#### Byte order of the registers
The `byte_order` setting specifies the byte and word-order of the registers. It
can be set to `ABCD` for _big endian (Motorola)_ or `DCBA` for _little endian
(Intel)_ format as well as `BADC` and `CDAB` for _big endian_ or _little endian_
with _byte swap_.
#### Measurement name
You can specify the name of the measurement for the fields defined in the
given section using the `measurement` setting. If the setting is omitted
`modbus` is used.
#### Optimization setting
__Please only use request optimization if you do understand the implications!__
The `optimization` setting can specified globally, i.e. __NOT__ per metric
section, and is used to optimize the actual requests sent to the device. Here,
the optimization is applied across _all metric sections_! The following
algorithms are available
##### `none` (_default_)
Do not perform any optimization. Please note that consecutive registers are
still grouped into one requests while obeying the maximum request sizes. This
setting should be used if you want to touch as less registers as possible at
the cost of more requests sent to the device.
##### `max_insert`
Fields are assigned to the same request as long as the hole between the touched
registers does not exceed the maximum fill size given via
`optimization_max_register_fill`. This optimization might lead to a drastically
reduced request number and thus an improved query time. The trade-off here is
between the cost of reading additional registers trashed later and the cost of
many requests.
__Please note:__ The optimal value for `optimization_max_register_fill` depends
on the network and the queried device. It is hence recommended to test several
values and assess performance in order to find the best value. Use the
`--test --debug` flags to monitor how may requests are sent and the number of
touched registers.
#### Field definitions
Each `metric` can contain a list of fields to collect from the modbus device.
The specified fields directly corresponds to the fields of the resulting metric.
##### register
The `register` setting specifies the modbus register-set to query and can be set
to `coil`, `discrete`, `holding` or `input`.
##### address
A field is identified by an `address` that reflects the modbus register
address. You can usually find the address values for the different data-points
in the datasheet of your modbus device. This is a mandatory setting.
For _coil_ and _discrete input_ registers this setting specifies the __bit__
containing the value of the field.
##### name
Using the `name` setting you can specify the field-name in the metric as output
by the plugin.
__Please note:__ There cannot be multiple fields with the same `name` in one
metric identified by `measurement`, `slave_id`, `register` and tag-set.
##### register datatype
The `type` setting specifies the datatype of the modbus register and can be
set to `INT8L`, `INT8H`, `UINT8L`, `UINT8H` where `L` is the lower byte of the
register and `H` is the higher byte.
Furthermore, the types `INT16`, `UINT16`, `INT32`, `UINT32`, `INT64` or `UINT64`
for integer types or `FLOAT16`, `FLOAT32` and `FLOAT64` for IEEE 754 binary
representations of floating point values exist. `FLOAT16` denotes a
half-precision float with a 16-bit representation.
Usually the datatype of the register is listed in the datasheet of your modbus
device in relation to the `address` described above.
This setting is ignored if the `register` is a bit-type (`coil` or `discrete`)
and can be omitted in these cases.
##### scaling
You can use the `scale` setting to scale the register values, e.g. if the
register contains a fix-point values in `UINT32` format with two decimal places
for example. To convert the read register value to the actual value you can set
the `scale=0.01`. The scale is used as a factor e.g. `field_value * scale`.
This setting is ignored if the `register` is a bit-type (`coil` or `discrete`)
and can be omitted in these cases.
__Please note:__ The resulting field-type will be set to `FLOAT64` if no output
format is specified.
##### output datatype
Using the `output` setting you can explicitly specify the output
field-datatype. The `output` type can be `INT64`, `UINT64` or `FLOAT64`. If not
set explicitly, the output type is guessed as follows: If `scale` is set to a
non-zero value, the output type is `FLOAT64`. Otherwise, the output type
corresponds to the register datatype _class_, i.e. `INT*` will result in
`INT64`, `UINT*` in `UINT64` and `FLOAT*` in `FLOAT64`.
In case the `register` is a bit-type (`coil` or `discrete`) only `UINT16` or
`BOOL` are valid with the former being the default if omitted. For `coil` and
`discrete` registers the field-value is output as zero or one in `UINT16` format
or as `true` and `false` in `BOOL` format.
#### Tags definitions
Each `metric` can be accompanied by a set of tag. These tags directly correspond
to the tags of the resulting metric.
__Please note:__ These tags take precedence over predefined tags such as `name`,
`type` or `slave_id`.
---
## Metrics
Metrics are custom and configured using the `discrete_inputs`, `coils`,

View File

@ -0,0 +1,371 @@
package modbus
import (
_ "embed"
"errors"
"fmt"
"hash/maphash"
"github.com/influxdata/telegraf"
)
//go:embed sample_metric.conf
var sampleConfigPartPerMetric string
type metricFieldDefinition struct {
RegisterType string `toml:"register"`
Address uint16 `toml:"address"`
Name string `toml:"name"`
InputType string `toml:"type"`
Scale float64 `toml:"scale"`
OutputType string `toml:"output"`
}
type metricDefinition struct {
SlaveID byte `toml:"slave_id"`
ByteOrder string `toml:"byte_order"`
Measurement string `toml:"measurement"`
Fields []metricFieldDefinition `toml:"fields"`
Tags map[string]string `toml:"tags"`
}
type ConfigurationPerMetric struct {
Optimization string `toml:"optimization"`
MaxExtraRegisters uint16 `toml:"optimization_max_register_fill"`
Metrics []metricDefinition `toml:"metric"`
workarounds ModbusWorkarounds
logger telegraf.Logger
}
func (c *ConfigurationPerMetric) SampleConfigPart() string {
return sampleConfigPartPerMetric
}
func (c *ConfigurationPerMetric) Check() error {
seed := maphash.MakeSeed()
seenFields := make(map[uint64]bool)
// Check optimization algorithm
switch c.Optimization {
case "", "none":
c.Optimization = "none"
case "max_insert":
if c.MaxExtraRegisters == 0 {
c.MaxExtraRegisters = 50
}
default:
return fmt.Errorf("unknown optimization %q", c.Optimization)
}
for defidx, def := range c.Metrics {
// Check byte order of the data
switch def.ByteOrder {
case "":
def.ByteOrder = "ABCD"
case "ABCD", "DCBA", "BADC", "CDAB", "MSW-BE", "MSW-LE", "LSW-LE", "LSW-BE":
default:
return fmt.Errorf("unknown byte-order %q", def.ByteOrder)
}
// Set the default for measurement if required
if def.Measurement == "" {
def.Measurement = "modbus"
}
// Reject any configuration without fields as it
// makes no sense to not define anything but a request.
if len(def.Fields) == 0 {
return errors.New("found request section without fields")
}
// Check the fields
for fidx, f := range def.Fields {
// Name is mandatory
if f.Name == "" {
return fmt.Errorf("empty field name in request for slave %d", def.SlaveID)
}
// Check register type
switch f.RegisterType {
case "":
f.RegisterType = "holding"
case "coil", "discrete", "holding", "input":
default:
return fmt.Errorf("unknown register-type %q for field %q", f.RegisterType, f.Name)
}
// Check the input and output type for all fields as we later need
// it to determine the number of registers to query.
switch f.RegisterType {
case "holding", "input":
// Check the input type
switch f.InputType {
case "":
case "INT8L", "INT8H", "INT16", "INT32", "INT64":
case "UINT8L", "UINT8H", "UINT16", "UINT32", "UINT64":
case "FLOAT16", "FLOAT32", "FLOAT64":
default:
return fmt.Errorf("unknown register data-type %q for field %q", f.InputType, f.Name)
}
// Check output type
switch f.OutputType {
case "", "INT64", "UINT64", "FLOAT64":
default:
return fmt.Errorf("unknown output data-type %q for field %q", f.OutputType, f.Name)
}
case "coil", "discrete":
// Bit register types can only be UINT64 or BOOL
switch f.OutputType {
case "", "UINT16", "BOOL":
default:
return fmt.Errorf("unknown output data-type %q for field %q", f.OutputType, f.Name)
}
}
def.Fields[fidx] = f
// Check for duplicate field definitions
id, err := c.fieldID(seed, def, f)
if err != nil {
return fmt.Errorf("cannot determine field id for %q: %w", f.Name, err)
}
if seenFields[id] {
return fmt.Errorf("field %q duplicated in measurement %q (slave %d)", f.Name, def.Measurement, def.SlaveID)
}
seenFields[id] = true
}
c.Metrics[defidx] = def
}
return nil
}
func (c *ConfigurationPerMetric) Process() (map[byte]requestSet, error) {
collection := make(map[byte]map[string][]field)
// Collect the requested registers across metrics and transform them into
// requests. This will produce one request per slave and register-type
for _, def := range c.Metrics {
// Make sure we have a set to work with
set, found := collection[def.SlaveID]
if !found {
set = make(map[string][]field)
}
for _, fdef := range def.Fields {
// Construct the field from the field definition
f, err := c.newField(fdef, def)
if err != nil {
return nil, fmt.Errorf("initializing field %q of measurement %q failed: %w", fdef.Name, def.Measurement, err)
}
// Attach the field to the correct set
set[fdef.RegisterType] = append(set[fdef.RegisterType], f)
}
collection[def.SlaveID] = set
}
result := make(map[byte]requestSet)
params := groupingParams{
Optimization: c.Optimization,
MaxExtraRegisters: c.MaxExtraRegisters,
Log: c.logger,
}
for sid, scollection := range collection {
var set requestSet
for registerType, fields := range scollection {
switch registerType {
case "coil":
params.MaxBatchSize = maxQuantityCoils
if c.workarounds.OnRequestPerField {
params.MaxBatchSize = 1
}
params.EnforceFromZero = c.workarounds.ReadCoilsStartingAtZero
requests := groupFieldsToRequests(fields, params)
set.coil = append(set.coil, requests...)
case "discrete":
params.MaxBatchSize = maxQuantityDiscreteInput
if c.workarounds.OnRequestPerField {
params.MaxBatchSize = 1
}
requests := groupFieldsToRequests(fields, params)
set.discrete = append(set.discrete, requests...)
case "holding":
params.MaxBatchSize = maxQuantityHoldingRegisters
if c.workarounds.OnRequestPerField {
params.MaxBatchSize = 1
}
requests := groupFieldsToRequests(fields, params)
set.holding = append(set.holding, requests...)
case "input":
params.MaxBatchSize = maxQuantityInputRegisters
if c.workarounds.OnRequestPerField {
params.MaxBatchSize = 1
}
requests := groupFieldsToRequests(fields, params)
set.input = append(set.input, requests...)
default:
return nil, fmt.Errorf("unknown register type %q", registerType)
}
}
if !set.Empty() {
result[sid] = set
}
}
return result, nil
}
func (c *ConfigurationPerMetric) newField(def metricFieldDefinition, mdef metricDefinition) (field, error) {
typed := def.RegisterType == "holding" || def.RegisterType == "input"
fieldLength := uint16(1)
if typed {
var err error
if fieldLength, err = c.determineFieldLength(def.InputType); err != nil {
return field{}, err
}
}
// Initialize the field
f := field{
measurement: mdef.Measurement,
name: def.Name,
address: def.Address,
length: fieldLength,
tags: mdef.Tags,
}
// Handle type conversions for coil and discrete registers
if !typed {
var err error
f.converter, err = determineUntypedConverter(def.OutputType)
if err != nil {
return field{}, err
}
// No more processing for un-typed (coil and discrete registers) fields
return f, nil
}
// Automagically determine the output type...
if def.OutputType == "" {
if def.Scale == 0.0 {
// For non-scaling cases we should choose the output corresponding to the input class
// i.e. INT64 for INT*, UINT64 for UINT* etc.
var err error
if def.OutputType, err = c.determineOutputDatatype(def.InputType); err != nil {
return field{}, err
}
} else {
// For scaling cases we always want FLOAT64 by default
def.OutputType = "FLOAT64"
}
}
// Setting default byte-order
byteOrder := mdef.ByteOrder
if byteOrder == "" {
byteOrder = "ABCD"
}
// Normalize the data relevant for determining the converter
inType, err := normalizeInputDatatype(def.InputType)
if err != nil {
return field{}, err
}
outType, err := normalizeOutputDatatype(def.OutputType)
if err != nil {
return field{}, err
}
order, err := normalizeByteOrder(byteOrder)
if err != nil {
return field{}, err
}
f.converter, err = determineConverter(inType, order, outType, def.Scale)
if err != nil {
return field{}, err
}
return f, nil
}
func (c *ConfigurationPerMetric) fieldID(seed maphash.Seed, def metricDefinition, field metricFieldDefinition) (uint64, error) {
var mh maphash.Hash
mh.SetSeed(seed)
if err := mh.WriteByte(def.SlaveID); err != nil {
return 0, err
}
if err := mh.WriteByte(0); err != nil {
return 0, err
}
if _, err := mh.WriteString(field.RegisterType); err != nil {
return 0, err
}
if err := mh.WriteByte(0); err != nil {
return 0, err
}
if _, err := mh.WriteString(def.Measurement); err != nil {
return 0, err
}
if err := mh.WriteByte(0); err != nil {
return 0, err
}
if _, err := mh.WriteString(field.Name); err != nil {
return 0, err
}
if err := mh.WriteByte(0); err != nil {
return 0, err
}
// Tags
for k, v := range def.Tags {
if _, err := mh.WriteString(k); err != nil {
return 0, err
}
if err := mh.WriteByte('='); err != nil {
return 0, err
}
if _, err := mh.WriteString(v); err != nil {
return 0, err
}
if err := mh.WriteByte(':'); err != nil {
return 0, err
}
}
if err := mh.WriteByte(0); err != nil {
return 0, err
}
return mh.Sum64(), nil
}
func (c *ConfigurationPerMetric) determineOutputDatatype(input string) (string, error) {
// Handle our special types
switch input {
case "INT8L", "INT8H", "INT16", "INT32", "INT64":
return "INT64", nil
case "UINT8L", "UINT8H", "UINT16", "UINT32", "UINT64":
return "UINT64", nil
case "FLOAT16", "FLOAT32", "FLOAT64":
return "FLOAT64", nil
}
return "unknown", fmt.Errorf("invalid input datatype %q for determining output", input)
}
func (c *ConfigurationPerMetric) determineFieldLength(input string) (uint16, error) {
// Handle our special types
switch input {
case "INT8L", "INT8H", "UINT8L", "UINT8H":
return 1, nil
case "INT16", "UINT16", "FLOAT16":
return 1, nil
case "INT32", "UINT32", "FLOAT32":
return 2, nil
case "INT64", "UINT64", "FLOAT64":
return 4, nil
}
return 0, fmt.Errorf("invalid input datatype %q for determining field length", input)
}

View File

@ -0,0 +1,311 @@
package modbus
import (
"testing"
"time"
mb "github.com/grid-x/modbus"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/metric"
"github.com/influxdata/telegraf/testutil"
"github.com/stretchr/testify/require"
"github.com/tbrandon/mbserver"
)
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(3.1415927410125732421875)
}
// 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",
},
},
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",
},
},
},
}
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, 1)
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),
},
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),
),
}
actual := acc.GetTelegrafMetrics()
testutil.RequireMetricsEqual(t, expected, actual, testutil.IgnoreTime())
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -63,6 +63,7 @@ type Modbus struct {
// Configuration type specific settings
ConfigurationOriginal
ConfigurationPerRequest
ConfigurationPerMetric
// Connection handling
client mb.Client
@ -97,6 +98,7 @@ type field struct {
omit bool
converter fieldConverterFunc
value interface{}
tags map[string]string
}
const (
@ -108,10 +110,11 @@ const (
// SampleConfig returns a basic configuration for the plugin
func (m *Modbus) SampleConfig() string {
configs := []Configuration{}
cfgOriginal := m.ConfigurationOriginal
cfgPerRequest := m.ConfigurationPerRequest
configs = append(configs, &cfgOriginal, &cfgPerRequest)
configs := []Configuration{
&m.ConfigurationOriginal,
&m.ConfigurationPerRequest,
&m.ConfigurationPerMetric,
}
totalConfig := sampleConfigStart
for _, c := range configs {
@ -143,6 +146,10 @@ func (m *Modbus) Init() error {
m.ConfigurationPerRequest.workarounds = m.Workarounds
m.ConfigurationPerRequest.logger = m.Log
cfg = &m.ConfigurationPerRequest
case "metric":
m.ConfigurationPerMetric.workarounds = m.Workarounds
m.ConfigurationPerMetric.logger = m.Log
cfg = &m.ConfigurationPerMetric
default:
return fmt.Errorf("unknown configuration type %q", m.ConfigurationType)
}
@ -503,16 +510,15 @@ func (m *Modbus) gatherRequestsInput(requests []request) error {
func (m *Modbus) collectFields(acc telegraf.Accumulator, timestamp time.Time, tags map[string]string, requests []request) {
grouper := metric.NewSeriesGrouper()
for _, request := range requests {
// Collect tags from global and per-request
rtags := map[string]string{}
for k, v := range tags {
rtags[k] = v
}
for k, v := range request.tags {
rtags[k] = v
}
for _, field := range request.fields {
// Collect tags from global and per-request
ftags := map[string]string{}
for k, v := range tags {
ftags[k] = v
}
for k, v := range field.tags {
ftags[k] = v
}
// In case no measurement was specified we use "modbus" as default
measurement := "modbus"
if field.measurement != "" {
@ -520,7 +526,7 @@ func (m *Modbus) collectFields(acc telegraf.Accumulator, timestamp time.Time, ta
}
// Group the data by series
grouper.Add(measurement, rtags, timestamp, field.name, field.value)
grouper.Add(measurement, ftags, timestamp, field.name, field.value)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,6 @@ type request struct {
address uint16
length uint16
fields []field
tags map[string]string
}
func countRegisters(requests []request) uint64 {
@ -203,6 +202,14 @@ func groupFieldsToRequests(fields []field, params groupingParams) []request {
var groups []request
var current request
for _, f := range fields {
// Add tags from higher up
if f.tags == nil {
f.tags = make(map[string]string, len(params.Tags))
}
for k, v := range params.Tags {
f.tags[k] = v
}
// Check if we need to interrupt the current chunk and require a new one
if current.length > 0 && f.address == current.address+current.length {
// Still safe to add the field to the current request
@ -285,12 +292,5 @@ func groupFieldsToRequests(fields []field, params groupingParams) []request {
}
}
// Copy the tags
for i := range requests {
requests[i].tags = make(map[string]string)
for k, v := range params.Tags {
requests[i].tags[k] = v
}
}
return requests
}

View File

@ -49,4 +49,5 @@
## Define the configuration schema
## |---register -- define fields per register type in the original style (only supports one slave ID)
## |---request -- define fields on a requests base
## |---metric -- define fields on a metric base
configuration_type = "register"

View File

@ -0,0 +1,67 @@
## --- "metric" configuration style ---
## Per metric definition
##
## Request optimization algorithm across metrics
## |---none -- Do not perform any optimization and just group requests
## | within metrics (default)
## |---max_insert -- Collate registers across all defined metrics and fill in
## holes to optimize the number of requests.
# optimization = "none"
## Maximum number of registers the optimizer is allowed to insert between
## non-consecutive registers to save requests.
## This option is only used for the 'max_insert' optimization strategy and
## effectively denotes the hole size between registers to fill.
# optimization_max_register_fill = 50
## Define a metric produced by the requests to the device
## Multiple of those metrics can be defined. The referenced registers will
## be collated into requests send to the device
[[inputs.modbus.metric]]
## ID of the modbus slave device to query
## If you need to query multiple slave-devices, create several "metric" definitions.
slave_id = 1
## Byte order of the data
## |---ABCD -- Big Endian (Motorola)
## |---DCBA -- Little Endian (Intel)
## |---BADC -- Big Endian with byte swap
## |---CDAB -- Little Endian with byte swap
# byte_order = "ABCD"
## Name of the measurement
# measurement = "modbus"
## Field definitions
## register - type of the modbus register, can be "coil", "discrete",
## "holding" or "input". Defaults to "holding".
## address - address of the register to query. For coil and discrete inputs this is the bit address.
## name - field name
## type *1 - type of the modbus field, can be
## INT8L, INT8H, UINT8L, UINT8H (low and high byte variants)
## INT16, UINT16, INT32, UINT32, INT64, UINT64 and
## FLOAT16, FLOAT32, FLOAT64 (IEEE 754 binary representation)
## scale *1 - (optional) factor to scale the variable with
## output *2 - (optional) type of resulting field, can be INT64, UINT64 or FLOAT64. Defaults to FLOAT64 if
## "scale" is provided and to the input "type" class otherwise (i.e. INT* -> INT64, etc).
##
## *1: These fields are ignored for both "coil" and "discrete"-input type of registers.
## *2: This field can only be "UINT16" or "BOOL" if specified for both "coil"
## and "discrete"-input type of registers. By default the fields are
## output as zero or one in UINT16 format unless "BOOL" is used.
fields = [
{ register="coil", address=0, name="door_open"},
{ register="coil", address=1, name="status_ok"},
{ register="holding", address=0, name="voltage", type="INT16" },
{ address=1, name="current", type="INT32", scale=0.001 },
{ address=5, name="energy", type="FLOAT32", scale=0.001,},
{ address=7, name="frequency", type="UINT32", scale=0.1 },
{ address=8, name="power_factor", type="INT64", scale=0.01 },
]
## Tags assigned to the metric
# [inputs.modbus.metric.tags]
# machine = "impresser"
# location = "main building"

View File

@ -13,6 +13,17 @@ func firstSection(t *T, root ast.Node) error {
var n ast.Node
n = root.FirstChild()
// Ignore HTML comments such as linter ignore sections
for {
if n == nil {
break
}
if _, ok := n.(*ast.HTMLBlock); !ok {
break
}
n = n.NextSibling()
}
t.assertKind(ast.KindHeading, n)
t.assertHeadingLevel(1, n)
t.assertFirstChildRegexp(` Plugin$`, n)