feat(inputs.modbus): Add per-metric configuration style (#13507)
This commit is contained in:
parent
657eca5cf0
commit
96b9845853
|
|
@ -1,3 +1,4 @@
|
||||||
|
<!-- markdownlint-disable MD024 -->
|
||||||
# Modbus Input Plugin
|
# Modbus Input Plugin
|
||||||
|
|
||||||
The Modbus plugin collects Discrete Inputs, Coils, Input Registers and Holding
|
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
|
## 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
|
# Retrieve data from MODBUS slave devices
|
||||||
[[inputs.modbus]]
|
[[inputs.modbus]]
|
||||||
## Connection Configuration
|
## Connection Configuration
|
||||||
|
|
@ -66,6 +67,7 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
|
||||||
## Define the configuration schema
|
## Define the configuration schema
|
||||||
## |---register -- define fields per register type in the original style (only supports one slave ID)
|
## |---register -- define fields per register type in the original style (only supports one slave ID)
|
||||||
## |---request -- define fields on a requests base
|
## |---request -- define fields on a requests base
|
||||||
|
## |---metric -- define fields on a metric base
|
||||||
configuration_type = "register"
|
configuration_type = "register"
|
||||||
|
|
||||||
## --- "register" configuration style ---
|
## --- "register" configuration style ---
|
||||||
|
|
@ -238,6 +240,74 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
|
||||||
machine = "impresser"
|
machine = "impresser"
|
||||||
location = "main building"
|
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.
|
## RS485 specific settings. Only take effect for serial controllers.
|
||||||
## Note: This has to be at the end of the modbus configuration due to
|
## Note: This has to be at the end of the modbus configuration due to
|
||||||
## TOML constraints.
|
## TOML constraints.
|
||||||
|
|
@ -304,6 +374,7 @@ Directly jump to the styles:
|
||||||
|
|
||||||
- [original / register plugin style](#register-configuration-style)
|
- [original / register plugin style](#register-configuration-style)
|
||||||
- [per-request style](#request-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
|
||||||
|
|
||||||
Metrics are custom and configured using the `discrete_inputs`, `coils`,
|
Metrics are custom and configured using the `discrete_inputs`, `coils`,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
@ -63,6 +63,7 @@ type Modbus struct {
|
||||||
// Configuration type specific settings
|
// Configuration type specific settings
|
||||||
ConfigurationOriginal
|
ConfigurationOriginal
|
||||||
ConfigurationPerRequest
|
ConfigurationPerRequest
|
||||||
|
ConfigurationPerMetric
|
||||||
|
|
||||||
// Connection handling
|
// Connection handling
|
||||||
client mb.Client
|
client mb.Client
|
||||||
|
|
@ -97,6 +98,7 @@ type field struct {
|
||||||
omit bool
|
omit bool
|
||||||
converter fieldConverterFunc
|
converter fieldConverterFunc
|
||||||
value interface{}
|
value interface{}
|
||||||
|
tags map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -108,10 +110,11 @@ const (
|
||||||
|
|
||||||
// SampleConfig returns a basic configuration for the plugin
|
// SampleConfig returns a basic configuration for the plugin
|
||||||
func (m *Modbus) SampleConfig() string {
|
func (m *Modbus) SampleConfig() string {
|
||||||
configs := []Configuration{}
|
configs := []Configuration{
|
||||||
cfgOriginal := m.ConfigurationOriginal
|
&m.ConfigurationOriginal,
|
||||||
cfgPerRequest := m.ConfigurationPerRequest
|
&m.ConfigurationPerRequest,
|
||||||
configs = append(configs, &cfgOriginal, &cfgPerRequest)
|
&m.ConfigurationPerMetric,
|
||||||
|
}
|
||||||
|
|
||||||
totalConfig := sampleConfigStart
|
totalConfig := sampleConfigStart
|
||||||
for _, c := range configs {
|
for _, c := range configs {
|
||||||
|
|
@ -143,6 +146,10 @@ func (m *Modbus) Init() error {
|
||||||
m.ConfigurationPerRequest.workarounds = m.Workarounds
|
m.ConfigurationPerRequest.workarounds = m.Workarounds
|
||||||
m.ConfigurationPerRequest.logger = m.Log
|
m.ConfigurationPerRequest.logger = m.Log
|
||||||
cfg = &m.ConfigurationPerRequest
|
cfg = &m.ConfigurationPerRequest
|
||||||
|
case "metric":
|
||||||
|
m.ConfigurationPerMetric.workarounds = m.Workarounds
|
||||||
|
m.ConfigurationPerMetric.logger = m.Log
|
||||||
|
cfg = &m.ConfigurationPerMetric
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unknown configuration type %q", m.ConfigurationType)
|
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) {
|
func (m *Modbus) collectFields(acc telegraf.Accumulator, timestamp time.Time, tags map[string]string, requests []request) {
|
||||||
grouper := metric.NewSeriesGrouper()
|
grouper := metric.NewSeriesGrouper()
|
||||||
for _, request := range requests {
|
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 {
|
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
|
// In case no measurement was specified we use "modbus" as default
|
||||||
measurement := "modbus"
|
measurement := "modbus"
|
||||||
if field.measurement != "" {
|
if field.measurement != "" {
|
||||||
|
|
@ -520,7 +526,7 @@ func (m *Modbus) collectFields(acc telegraf.Accumulator, timestamp time.Time, ta
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group the data by series
|
// 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
|
|
@ -10,7 +10,6 @@ type request struct {
|
||||||
address uint16
|
address uint16
|
||||||
length uint16
|
length uint16
|
||||||
fields []field
|
fields []field
|
||||||
tags map[string]string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func countRegisters(requests []request) uint64 {
|
func countRegisters(requests []request) uint64 {
|
||||||
|
|
@ -203,6 +202,14 @@ func groupFieldsToRequests(fields []field, params groupingParams) []request {
|
||||||
var groups []request
|
var groups []request
|
||||||
var current request
|
var current request
|
||||||
for _, f := range fields {
|
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
|
// Check if we need to interrupt the current chunk and require a new one
|
||||||
if current.length > 0 && f.address == current.address+current.length {
|
if current.length > 0 && f.address == current.address+current.length {
|
||||||
// Still safe to add the field to the current request
|
// 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
|
return requests
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,4 +49,5 @@
|
||||||
## Define the configuration schema
|
## Define the configuration schema
|
||||||
## |---register -- define fields per register type in the original style (only supports one slave ID)
|
## |---register -- define fields per register type in the original style (only supports one slave ID)
|
||||||
## |---request -- define fields on a requests base
|
## |---request -- define fields on a requests base
|
||||||
|
## |---metric -- define fields on a metric base
|
||||||
configuration_type = "register"
|
configuration_type = "register"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -13,6 +13,17 @@ func firstSection(t *T, root ast.Node) error {
|
||||||
var n ast.Node
|
var n ast.Node
|
||||||
n = root.FirstChild()
|
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.assertKind(ast.KindHeading, n)
|
||||||
t.assertHeadingLevel(1, n)
|
t.assertHeadingLevel(1, n)
|
||||||
t.assertFirstChildRegexp(` Plugin$`, n)
|
t.assertFirstChildRegexp(` Plugin$`, n)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue